Ticket #1115: paginate.py.diff.1

File paginate.py.diff.1, 10.6 kB (added by randall@tnr.cc, 2 years ago)

patch for paginate.py

Line 
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