| 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 |
|---|