| 1 | Index: paginate.py |
|---|
| 2 | =================================================================== |
|---|
| 3 | --- paginate.py (revision 1888) |
|---|
| 4 | +++ paginate.py (working copy) |
|---|
| 5 | @@ -1,3 +1,5 @@ |
|---|
| 6 | +import re |
|---|
| 7 | +import types |
|---|
| 8 | from math import ceil |
|---|
| 9 | import logging |
|---|
| 10 | |
|---|
| 11 | @@ -2,4 +4,12 @@ |
|---|
| 12 | import cherrypy |
|---|
| 13 | +import sqlobject |
|---|
| 14 | from sqlobject.main import SelectResults |
|---|
| 15 | |
|---|
| 16 | +try: |
|---|
| 17 | + # Can't depend on sqlalchemy being available. |
|---|
| 18 | + import sqlalchemy |
|---|
| 19 | + from sqlalchemy.ext.selectresults import SelectResults as SASelectResults |
|---|
| 20 | +except ImportError: |
|---|
| 21 | + SASelectResults = None |
|---|
| 22 | + |
|---|
| 23 | import turbogears |
|---|
| 24 | @@ -16,15 +26,19 @@ |
|---|
| 25 | def decorated(func, *args, **kw): |
|---|
| 26 | page = int(kw.pop('tg_paginate_no', 1)) |
|---|
| 27 | limit_ = int(kw.pop('tg_paginate_limit', limit)) |
|---|
| 28 | - order = kw.pop('tg_paginate_order', default_order) |
|---|
| 29 | - reversed = kw.pop('tg_paginate_reversed', None) |
|---|
| 30 | - |
|---|
| 31 | + order = kw.pop('tg_paginate_order', None) |
|---|
| 32 | + ordering = kw.pop('tg_paginate_ordering', None) |
|---|
| 33 | + |
|---|
| 34 | + # Convert ordering str to a dict. |
|---|
| 35 | + if ordering: |
|---|
| 36 | + ordering = convert_ordering(ordering) |
|---|
| 37 | + |
|---|
| 38 | if not allow_limit_override: |
|---|
| 39 | limit_ = limit |
|---|
| 40 | + |
|---|
| 41 | + log.debug("Pagination params: page=%s, limit=%s, order=%s " |
|---|
| 42 | + "", page, limit_, order) |
|---|
| 43 | |
|---|
| 44 | - log.debug("Pagination params: page=%s, limit=%s, order=%s, " |
|---|
| 45 | - "reversed=%s", page, limit_, order, reversed) |
|---|
| 46 | - |
|---|
| 47 | # get the output from the decorated function |
|---|
| 48 | output = func(*args, **kw) |
|---|
| 49 | if not isinstance(output, dict): |
|---|
| 50 | @@ -37,18 +51,32 @@ |
|---|
| 51 | if order and not default_order: |
|---|
| 52 | raise "If you want to enable ordering you need " \ |
|---|
| 53 | "to provide a default_order" |
|---|
| 54 | - |
|---|
| 55 | + elif default_order and not ordering: |
|---|
| 56 | + ordering = {default_order:[0, True]} |
|---|
| 57 | + elif ordering and order: |
|---|
| 58 | + sort_ordering(ordering, order) |
|---|
| 59 | + log.info('ordering %s' % ordering) |
|---|
| 60 | + |
|---|
| 61 | row_count = 0 |
|---|
| 62 | - if isinstance(var_data, SelectResults): |
|---|
| 63 | + if isinstance(var_data, SelectResults) or \ |
|---|
| 64 | + (SASelectResults and isinstance(var_data, SASelectResults)): |
|---|
| 65 | row_count = var_data.count() |
|---|
| 66 | - col = getattr(var_data.sourceClass.q, order, None) |
|---|
| 67 | - if default_order: |
|---|
| 68 | - if col: |
|---|
| 69 | - var_data = var_data.orderBy(col) |
|---|
| 70 | + if ordering: |
|---|
| 71 | + # Build order_by list. |
|---|
| 72 | + order_cols = range(len(ordering)) |
|---|
| 73 | + for (colname, order_opts) in ordering.items(): |
|---|
| 74 | + col = sql_get_column(colname, var_data) |
|---|
| 75 | + if not col: |
|---|
| 76 | + raise StandardError, "The order column (%s) doesn't exist" % colname |
|---|
| 77 | + order_by_expr = sql_order_col(col, order_opts[1]) |
|---|
| 78 | + order_cols[order_opts[0]] = order_by_expr |
|---|
| 79 | + # May need to address potential of ordering already |
|---|
| 80 | + # existing in var_data. |
|---|
| 81 | + # SO and SA differ on this method name. |
|---|
| 82 | + if hasattr(var_data, 'orderBy'): |
|---|
| 83 | + var_data = var_data.orderBy(order_cols) |
|---|
| 84 | else: |
|---|
| 85 | - raise "The order column (%s) doesn't exist" % order |
|---|
| 86 | - if reversed: |
|---|
| 87 | - var_data = var_data.reversed() |
|---|
| 88 | + var_data = var_data.order_by(order_cols) |
|---|
| 89 | elif isinstance(var_data, list): |
|---|
| 90 | row_count = len(var_data) |
|---|
| 91 | else: |
|---|
| 92 | @@ -69,15 +97,15 @@ |
|---|
| 93 | #input_values = cherrypy.request.input_values.copy() |
|---|
| 94 | input_values = kw.copy() |
|---|
| 95 | input_values.pop('self', None) |
|---|
| 96 | - |
|---|
| 97 | + |
|---|
| 98 | cherrypy.request.paginate = Paginate(current_page=page, |
|---|
| 99 | - limit=limit_, |
|---|
| 100 | - pages=pages_to_show, |
|---|
| 101 | - page_count=page_count, |
|---|
| 102 | - input_values=input_values, |
|---|
| 103 | - order=order, |
|---|
| 104 | - reversed=reversed) |
|---|
| 105 | - |
|---|
| 106 | + limit=limit_, |
|---|
| 107 | + pages=pages_to_show, |
|---|
| 108 | + page_count=page_count, |
|---|
| 109 | + input_values=input_values, |
|---|
| 110 | + order=order, |
|---|
| 111 | + ordering=ordering) |
|---|
| 112 | + |
|---|
| 113 | # we replace the var with the sliced one |
|---|
| 114 | endpoint = offset + limit_ |
|---|
| 115 | log.debug("slicing data between %d and %d", offset, endpoint) |
|---|
| 116 | @@ -98,18 +126,30 @@ |
|---|
| 117 | class Paginate: |
|---|
| 118 | """class for variable provider""" |
|---|
| 119 | def __init__(self, current_page, pages, page_count, input_values, |
|---|
| 120 | - limit, order, reversed): |
|---|
| 121 | + limit, order, ordering): |
|---|
| 122 | + # If we have a nested dict from a form, it must be denormalized before |
|---|
| 123 | + # it can be put into a GET URL. |
|---|
| 124 | + # {'form_field':{'text':'hello', 'hidden':''} |
|---|
| 125 | + # Goes to: |
|---|
| 126 | + # {'form_field.text':'hello', 'form_field.hidden':''} |
|---|
| 127 | + # I'm going to be specific so as to minimize breakage. |
|---|
| 128 | + for (ikey, ivalue) in input_values.items(): |
|---|
| 129 | + if isinstance(ivalue, dict) and len(ivalue) > 1: |
|---|
| 130 | + del input_values[ikey] |
|---|
| 131 | + for (sub_ikey, sub_ivalue) in ivalue.items(): |
|---|
| 132 | + new_key = '%s.%s' % (ikey, sub_ikey) |
|---|
| 133 | + input_values[new_key] = sub_ivalue |
|---|
| 134 | + |
|---|
| 135 | self.pages = pages |
|---|
| 136 | self.limit = limit |
|---|
| 137 | self.page_count = page_count |
|---|
| 138 | self.current_page = current_page |
|---|
| 139 | self.input_values = input_values |
|---|
| 140 | self.order = order |
|---|
| 141 | - self.reversed = reversed |
|---|
| 142 | - |
|---|
| 143 | + self.ordering = ordering |
|---|
| 144 | + |
|---|
| 145 | self.input_values.update(dict(tg_paginate_limit=limit, |
|---|
| 146 | - tg_paginate_order=order, |
|---|
| 147 | - tg_paginate_reversed=reversed)) |
|---|
| 148 | + tg_paginate_ordering=ordering)) |
|---|
| 149 | if current_page < page_count: |
|---|
| 150 | self.input_values.update(dict( |
|---|
| 151 | tg_paginate_no=current_page+1, |
|---|
| 152 | @@ -136,31 +176,17 @@ |
|---|
| 153 | self.href_prev = None |
|---|
| 154 | self.href_first = None |
|---|
| 155 | |
|---|
| 156 | - def get_href(self, page, order=None, reverse_order=None): |
|---|
| 157 | + def get_href(self, page, order=None, reversed=None): |
|---|
| 158 | + # Note that reversed is not used. It should be cleaned up here and in |
|---|
| 159 | + # the template. I'm not removing it now because I don't want to break |
|---|
| 160 | + # the API. |
|---|
| 161 | + order = order or None |
|---|
| 162 | + self.input_values['tg_paginate_no'] = page |
|---|
| 163 | if order: |
|---|
| 164 | - if order == self.order: |
|---|
| 165 | - if self.reversed: |
|---|
| 166 | - reversed = None |
|---|
| 167 | - else: |
|---|
| 168 | - reversed = True |
|---|
| 169 | - else: |
|---|
| 170 | - reversed = None |
|---|
| 171 | - if reverse_order: |
|---|
| 172 | - if reversed: |
|---|
| 173 | - reversed = None |
|---|
| 174 | - else: |
|---|
| 175 | - reversed = True |
|---|
| 176 | - else: |
|---|
| 177 | - order = self.order |
|---|
| 178 | - reversed = self.reversed |
|---|
| 179 | - |
|---|
| 180 | - self.input_values.update(dict(tg_paginate_no=page, |
|---|
| 181 | - tg_paginate_order=order, |
|---|
| 182 | - tg_paginate_reversed=reversed)) |
|---|
| 183 | - |
|---|
| 184 | + self.input_values['tg_paginate_order'] = order |
|---|
| 185 | + |
|---|
| 186 | return turbogears.url('', self.input_values) |
|---|
| 187 | |
|---|
| 188 | - |
|---|
| 189 | def _select_pages_to_show(current_page, page_count, max_pages): |
|---|
| 190 | pages_to_show = [] |
|---|
| 191 | |
|---|
| 192 | @@ -186,3 +212,70 @@ |
|---|
| 193 | end = page_count |
|---|
| 194 | |
|---|
| 195 | return range(start, end+1) |
|---|
| 196 | + |
|---|
| 197 | +def sort_ordering(ordering, sort_name): |
|---|
| 198 | + """Rearrange ordering based on sort_name.""" |
|---|
| 199 | + log.info('sort called with %s and %s' % (ordering, sort_name)) |
|---|
| 200 | + if sort_name not in ordering: |
|---|
| 201 | + ordering[sort_name] = [-1, True] |
|---|
| 202 | + if ordering[sort_name][0] == 0: |
|---|
| 203 | + # Flip |
|---|
| 204 | + ordering[sort_name][1] = not ordering[sort_name][1] |
|---|
| 205 | + else: |
|---|
| 206 | + ordering[sort_name][0] = 0 |
|---|
| 207 | + for key in ordering.keys(): |
|---|
| 208 | + if key != sort_name and ordering[key][0] < len(ordering) - 1: |
|---|
| 209 | + ordering[key][0] += 1 |
|---|
| 210 | + |
|---|
| 211 | +def sql_get_column(colname, var_data): |
|---|
| 212 | + """Return a column from var_data based on colname.""" |
|---|
| 213 | + if isinstance(var_data, SelectResults): |
|---|
| 214 | + col = getattr(var_data.sourceClass.q, colname, None) |
|---|
| 215 | + elif isinstance(var_data, SASelectResults): |
|---|
| 216 | + col = getattr(var_data._query.table.c, colname, None) |
|---|
| 217 | + else: |
|---|
| 218 | + raise StandardError, 'expected SelectResults' |
|---|
| 219 | + return col |
|---|
| 220 | + |
|---|
| 221 | +def sql_order_col(col, ascending=True): |
|---|
| 222 | + """Return an ordered col for col.""" |
|---|
| 223 | + if isinstance(col, sqlalchemy.schema.Column): |
|---|
| 224 | + if ascending: |
|---|
| 225 | + order_col = sqlalchemy.sql.asc(col) |
|---|
| 226 | + else: |
|---|
| 227 | + order_col = sqlalchemy.sql.desc(col) |
|---|
| 228 | + elif isinstance(col, types.InstanceType): |
|---|
| 229 | + # I don't like using InstanceType, but that's what sqlobject col type |
|---|
| 230 | + # is. |
|---|
| 231 | + if ascending: |
|---|
| 232 | + order_col = col |
|---|
| 233 | + else: |
|---|
| 234 | + order_col = sqlobject.DESC(col) |
|---|
| 235 | + else: |
|---|
| 236 | + raise StandardError, 'expected Column, but got %s' % str(type(col)) |
|---|
| 237 | + return order_col |
|---|
| 238 | + |
|---|
| 239 | +# Ordering re: |
|---|
| 240 | +ordering_expr = re.compile(r"('\w+'): ?\[(\d+), ?(True|False)\]") |
|---|
| 241 | + |
|---|
| 242 | +def convert_ordering(ordering): |
|---|
| 243 | + """Covert ordering unicode string to dict.""" |
|---|
| 244 | + |
|---|
| 245 | + # eval would be simple, but insecure. |
|---|
| 246 | + if not isinstance(ordering, (str, unicode)): |
|---|
| 247 | + raise ValueError, "ordering should be string or unicode." |
|---|
| 248 | + new_ordering = {} |
|---|
| 249 | + try: |
|---|
| 250 | + ordering_info_find = ordering_expr.findall(ordering) |
|---|
| 251 | + emsg = "Didn't match ordering for %s." % str(ordering) |
|---|
| 252 | + assert len(ordering_info_find) > 0, emsg |
|---|
| 253 | + for ordering_info in ordering_info_find: |
|---|
| 254 | + ordering_key = str(ordering_info[0]).strip("'") |
|---|
| 255 | + ordering_order = int(ordering_info[1]) |
|---|
| 256 | + ordering_reverse = bool(ordering_info[2]) |
|---|
| 257 | + new_ordering[ordering_key] = [ordering_order, |
|---|
| 258 | + ordering_reverse] |
|---|
| 259 | + except StandardError, e: |
|---|
| 260 | + log.debug('FAILED to convert ordering.') |
|---|
| 261 | + new_ordering = None |
|---|
| 262 | + return new_ordering |
|---|