Package turbogears :: Module paginate'

Source Code for Module turbogears.paginate'

  1   
  2  import types 
  3  from math import ceil 
  4  import logging 
  5  import warnings 
  6   
  7  try: 
  8      set 
  9  except NameError: # Python 2.3 
 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  # lists of databases that lack support for OFFSET 
 35  # this will need to be updated periodically as modules change 
 36  _so_no_offset = 'mssql maxdb sybase'.split() 
 37  _sa_no_offset = 'mssql maxdb access'.split() 
 38   
 39  # this is a global that is set the first time paginate() is called 
 40  _simulate_offset = None 
 41   
 42  # these are helper classes for getting data that has no table column 
43 -class attrwrapper:
44 """Helper class for accessing objec attributes."""
45 - def __init__(self, name):
46 self.name = name
47 - def __call__(self, obj):
48 for name in self.name.split('.'): 49 obj = getattr(obj, name) 50 return obj
51
52 -class itemwrapper:
53 """Helper class for dicitionary access."""
54 - def __init__(self, name):
55 self.name = name
56 - def __call__(self, obj):
57 return obj[self.name]
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 # get the output from the decorated function 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 # e.g. SQLAlchemy query class 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 # adapt old style default_order to new style 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: # SQL query 214 row_count = var_data.count() or 0 215 except AttributeError: # other iterator 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 # If limit is zero then return all our rows 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 # remove pagination parameters from request 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 # we replace the var with the sliced one 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 # skip over the number of records specified by offset 289 for i in xrange(offset): 290 var_data.next() 291 # return the records that remain 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 # add hint that paginate parameters shall be left intact 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
316 -def _paginate_var_provider(d):
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
327 -class Paginate:
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 # If ordering is empty, don't add it. 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.