1
2 import types
3 from math import ceil
4 import logging
5 import warnings
6
7 try:
8 set
9 except NameError:
10 from sets import Set as set
11
12 import cherrypy
13 try:
14 import sqlobject
15 except ImportError:
16 sqlobject = None
17
18 try:
19 import sqlalchemy
20 except ImportError:
21 sqlalchemy = None
22
23 import turbogears
24 from turbogears.controllers import redirect
25 from turbogears.decorator import weak_signature_decorator
26 from turbogears.view import variable_providers
27 from formencode.variabledecode import variable_encode
28 from turbogears.widgets import PaginateDataGrid
29 from turbogears.util import add_tg_args
30
31 log = logging.getLogger("turbogears.paginate")
32
33
34
35
36 _so_no_offset = 'mssql maxdb sybase'.split()
37 _sa_no_offset = 'mssql maxdb access'.split()
38
39
40 _simulate_offset = None
41
42
44 """Helper class for accessing objec attributes."""
48 for name in self.name.split('.'):
49 obj = getattr(obj, name)
50 return obj
51
53 """Helper class for dicitionary access."""
58
59
60 -def paginate(var_name, default_order='', default_reversed=None, limit=10,
61 max_limit=0, allow_limit_override=None, max_pages=5,
62 max_sort=1000, dynamic_limit=None):
63 """The famous TurboGears paginate decorator.
64
65 @param var_name: The variable name that the paginate decorator will try
66 to control. This key must be present in the dictionnary returned from your
67 controller in order for the paginate decorator to be able to handle it.
68 @type var_name: string
69
70 @param default_order: The column name(s) that will be used to orde
71 pagination results. Due to the way pagination is implemented specifying a
72 default_order will override any result ordering performed in the controller.
73 @type default_order: string or a list of strings. Any string starting with
74 "-" (minus sign) indicates a reverse order for that field/column.
75
76 @param default_reversed: Deprecated, use default_order with minus sign.
77 @type default_reversed: Boolean
78
79 @param limit: The hard-coded limit that the paginate decorator will impose
80 on the number of "var_name" to display at the same time. This value can be
81 overridden by the use of the dynamic_limit keyword argument.
82 @type limit: integer
83
84 @param max_limit: The maximum number to which the imposed limit
85 can be increased using the dynamic_limit keyword argument in the URL.
86 If this is set to 0, no dynamic change at all will be allowed;
87 if it is set to None, any change will be allowed.
88 @type max_limit: int
89
90 @param allow_limit_override: Deprecated, use max_limit.
91 @type allow_limit_override: Boolean
92
93 @param max_pages: Used to generate the tg.paginate.pages variable. If the
94 page count is larger than max_pages, tg.paginate.pages will only contain
95 the page numbers surrounding the current page at a distance of max_pages/2.
96 A zero value means that all pages will be shown, no matter how much.
97 @type max_pages: integer
98
99 @param max_sort: The maximum number of records that will be sorted in
100 memory if the data cannot be sorted using SQL. If set to 0, sorting in
101 memory will never be performed; if set to None, no limit will be imposed.
102 @type max_sort: integer
103
104 @param dynamic_limit: If specified, this parameter must be the name
105 of a key present in the dictionary returned by your decorated
106 controller. The value found for this key will be used as the limit
107 for our pagination and will override the other settings, the hard-coded
108 one declared in the decorator itself AND the URL parameter one.
109 This enables the programmer to store a limit settings inside the
110 application preferences and then let the user manage it.
111 @type dynamic_limit: string
112
113 """
114
115 def entangle(func):
116
117 get = turbogears.config.get
118
119 def decorated(func, *args, **kw):
120
121 def kwpop(name, default=None):
122 return kw.pop(var_name + '_tgp_' + name,
123 kw.pop('tg_paginate_' + name, default))
124
125 if default_reversed is not None:
126 warnings.warn("default_reversed is deprecated."
127 " Use default_order='-field' to indicate"
128 " default reversed order, or"
129 " default_order=['field1', '-field2, 'field3']"
130 " for multiple fields.", DeprecationWarning, 2)
131 if allow_limit_override is not None:
132 warnings.warn("allow_limit_override is deprecated."
133 " Use max_limit to specify an upper bound for limit.",
134 DeprecationWarning, 2)
135
136 page = kwpop('no')
137 if page is None:
138 page = 1
139 elif page == 'last':
140 page = None
141 else:
142 try:
143 page = int(page)
144 if page < 1:
145 raise ValueError
146 except (TypeError, ValueError):
147 page = 1
148 if get('paginate.redirect_on_out_of_range'):
149 cherrypy.request.params[var_name + '_tgp_no'] = page
150 redirect(cherrypy.request.path_info, cherrypy.request.params)
151
152 try:
153 limit_ = int(kwpop('limit'))
154 if max_limit is not None:
155 if max_limit <= 0 and not allow_limit_override:
156 raise ValueError
157 limit_ = min(limit_, max_limit)
158 except (TypeError, ValueError):
159 limit_ = limit
160 order = kwpop('order')
161 ordering = kwpop('ordering')
162
163 log.debug("paginate params: page=%s, limit=%s, order=%s",
164 page, limit_, order)
165
166
167 output = func(*args, **kw)
168 if not isinstance(output, dict):
169 return output
170
171 try:
172 var_data = output[var_name]
173 except KeyError:
174 raise KeyError("paginate: var_name"
175 " (%s) not found in output dict" % var_name)
176 if not hasattr(var_data, '__getitem__') and callable(var_data):
177
178 var_data = var_data()
179 if not hasattr(var_data, '__getitem__'):
180 raise TypeError('Paginate variable is not a sequence')
181
182 if dynamic_limit:
183 try:
184 dyn_limit = output[dynamic_limit]
185 except KeyError:
186 raise KeyError("paginate: dynamic_limit"
187 " (%s) not found in output dict" % dynamic_limit)
188 limit_ = dyn_limit
189
190 if ordering:
191 ordering = str(ordering).split(',')
192 else:
193 ordering = default_order or []
194 if isinstance(ordering, basestring):
195
196 if default_reversed:
197 ordering = "-" + ordering
198 ordering = [ordering]
199 elif default_reversed:
200 raise ValueError("paginate: default_reversed (deprecated)"
201 " only allowed when default_order is a basestring")
202
203 if order:
204 order = str(order)
205 log.debug('paginate: ordering was %s, sort is %s',
206 ordering, order)
207 sort_ordering(ordering, order)
208 log.debug('paginate: ordering is %s', ordering)
209
210 try:
211 row_count = len(var_data)
212 except TypeError:
213 try:
214 row_count = var_data.count() or 0
215 except AttributeError:
216 var_data = list(var_data)
217 row_count = len(var_data)
218
219 if ordering:
220 var_data = sort_data(var_data, ordering,
221 max_sort is None or 0 < row_count <= max_sort)
222
223
224 if not limit_:
225 limit_ = row_count or 1
226
227 page_count = int(ceil(float(row_count)/limit_))
228
229 if page is None:
230 page = max(page_count, 1)
231 if get('paginate.redirect_on_last_page'):
232 cherrypy.request.params[var_name + '_tgp_no'] = page
233 redirect(cherrypy.request.path_info, cherrypy.request.params)
234 elif page > page_count:
235 page = max(page_count, 1)
236 if get('paginate.redirect_on_out_of_range'):
237 cherrypy.request.params[var_name + '_tgp_no'] = page
238 redirect(cherrypy.request.path_info, cherrypy.request.params)
239
240 offset = (page-1) * limit_
241
242 pages_to_show = _select_pages_to_show(page, page_count, max_pages)
243
244
245 input_values = variable_encode(cherrypy.request.params.copy())
246 input_values.pop('self', None)
247 for input_key in input_values.keys():
248 if (input_key.startswith(var_name + '_tgp_') or
249 input_key.startswith('tg_paginate_')):
250 del input_values[input_key]
251
252 paginate_instance = Paginate(
253 current_page=page,
254 limit=limit_,
255 pages=pages_to_show,
256 page_count=page_count,
257 input_values=input_values,
258 order=order,
259 ordering=ordering,
260 row_count=row_count,
261 var_name=var_name)
262
263 cherrypy.request.paginate = paginate_instance
264 if not hasattr(cherrypy.request, 'paginates'):
265 cherrypy.request.paginates = dict()
266 cherrypy.request.paginates[var_name] = paginate_instance
267
268
269 endpoint = offset + limit_
270 log.debug("paginate: slicing data between %d and %d",
271 offset, endpoint)
272
273 global _simulate_offset
274 if _simulate_offset is None:
275 _simulate_offset = get('paginate.simulate_offset', None)
276 if _simulate_offset is None:
277 _simulate_offset = False
278 so_db = get('sqlobject.dburi', 'NOMATCH:').split(':', 1)[0]
279 sa_db = get('sqlalchemy.dburi', 'NOMATCH:').split(':', 1)[0]
280 if so_db in _so_no_offset or sa_db in _sa_no_offset:
281 _simulate_offset = True
282 log.warning("paginate: simulating OFFSET,"
283 " paginate may be slow"
284 " (disable with paginate.simulate_offset=False)")
285
286 if _simulate_offset:
287 var_data = iter(var_data[:endpoint])
288
289 for i in xrange(offset):
290 var_data.next()
291
292 output[var_name] = list(var_data)
293 else:
294 try:
295 output[var_name] = var_data[offset:endpoint]
296 except TypeError:
297 for i in xrange(offset):
298 var_data.next()
299 output[var_name] = [var_data.next()
300 for i in xrange(offset, endpoint)]
301
302 return output
303
304 if not get('tg.strict_parameters', False):
305
306 args = set()
307 for arg in 'no', 'limit', 'order', 'ordering':
308 args.add(var_name + '_tgp_' + arg)
309 args.add('tg_paginate_' + arg)
310 add_tg_args(func, args)
311 return decorated
312
313 return weak_signature_decorator(entangle)
314
315
317 """Auxiliary function for providing the paginate variable."""
318 paginate = getattr(cherrypy.request, 'paginate', None)
319 if paginate:
320 d.update(dict(paginate=paginate))
321 paginates = getattr(cherrypy.request, 'paginates', None)
322 if paginates:
323 d.update(dict(paginates=paginates))
324 variable_providers.append(_paginate_var_provider)
325
326
328 """Class for paginate variable provider."""
329
330 - def __init__(self, current_page, pages, page_count, input_values,
331 limit, order, ordering, row_count, var_name):
332
333 self.var_name = var_name
334 self.pages = pages
335 self.limit = limit
336 self.page_count = page_count
337 self.current_page = current_page
338 self.input_values = input_values
339 self.order = order
340 self.ordering = ordering
341 self.row_count = row_count
342 self.first_item = page_count and ((current_page - 1) * limit + 1) or 0
343 self.last_item = min(current_page * limit, row_count)
344
345 self.reversed = ordering and ordering[0][0] == '-'
346
347
348 input_values = {var_name + '_tgp_limit': limit}
349 if ordering:
350 input_values[var_name + '_tgp_ordering'] = ','.join(ordering)
351 self.input_values.update(input_values)
352
353 if current_page < page_count:
354 self.input_values.