Package orm2 :: Package ui :: Package xist :: Module result_table
[hide private]
[frames] | no frames]

Source Code for Module orm2.ui.xist.result_table

  1  #!/usr/bin/env python 
  2  # -*- coding: iso-8859-1 -*- 
  3   
  4  ##  This file is part of orm, The Object Relational Membrane Version 2. 
  5  ## 
  6  ##  Copyright 2002-2006 by Diedrich Vorberg <diedrich@tux4web.de> 
  7  ## 
  8  ##  All Rights Reserved 
  9  ## 
 10  ##  For more Information on orm see the README file. 
 11  ## 
 12  ##  This program is free software; you can redistribute it and/or modify 
 13  ##  it under the terms of the GNU General Public License as published by 
 14  ##  the Free Software Foundation; either version 2 of the License, or 
 15  ##  (at your option) any later version. 
 16  ## 
 17  ##  This program is distributed in the hope that it will be useful, 
 18  ##  but WITHOUT ANY WARRANTY; without even the implied warranty of 
 19  ##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 20  ##  GNU General Public License for more details. 
 21  ## 
 22  ##  You should have received a copy of the GNU General Public License 
 23  ##  along with this program; if not, write to the Free Software 
 24  ##  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 25  ## 
 26  ##  I have added a copy of the GPL in the file gpl.txt. 
 27   
 28  """ 
 29  Provide a base class for result tables that can be sorted and filtered 
 30  according to different columns. 
 31  """ 
 32   
 33  import sys, urllib 
 34  from string import * 
 35  from sets import Set 
 36   
 37  from orm2 import sql 
 38   
 39  from ll.xist.ns import html, chars 
 40  from ll.xist.xsc import * 
 41   
 42  from views import xist_view 
 43   
44 -class query_params:
45 """ 46 This class holds a number of query_param instances. These 47 represent possible url parameter use to control the 48 result_table. It is usually called from within class definitions of 49 result_table's subclasses. 50 """
51 - def __init__(self, *params):
52 """ 53 Pass query_params as positionsl arguments. 54 """ 55 for param in params: 56 if not isinstance(param, query_param): 57 raise TypeError("All params in a query_href must be " 58 "query_param instances") 59 60 self.params = params
61
62 - def href(self, base_url, result_table, request, **kw):
63 """ 64 Construct a href with all the right parameters in it. 65 66 @param base_url: Absolute(?) url of the procedure or 67 external method or whatever calls the result table 68 @param result_table: The result table object 69 @param request: Dictionary style object containing the 70 url params of the current request, may be empty. 71 @param kw: You may pass param/value pairs as key word 72 arguments to change params with regard to the current set 73 passed in 'request' as well as to add new parameters. 74 """ 75 info = {} 76 77 for param in self.params: 78 if request.has_key(param.name): 79 value = request[param.name] 80 else: 81 value = param.default_value 82 83 if kw.has_key(param.name): 84 value = kw[param.name] 85 del kw[param.name] 86 87 param.validate(value) 88 89 info[param.name] = value 90 91 # The remaingin extra arguments, that cannot be validated, are 92 # added verbatim. 93 info.update(kw) 94 95 96 info = map(lambda tpl: "%s=%s" % (tpl[0], urllib.quote(str(tpl[1])),), 97 info.items()) 98 return base_url + "?" + join(info, "&")
99
100 - def validate_all(self, request):
101 """ 102 Use the information from the query_param objects to check all 103 the params in request we know about. 104 105 @raises ValueError: If a param is not set correctly. 106 """ 107 for param in self.params: 108 param.validate(request.get(param.name, param.default_value))
109
110 - def has_param(self, name):
111 """ 112 Indicate whether a param by that NAME exists in the param set. 113 """ 114 for param in self.params: 115 if param.name == name: 116 return True 117 118 return False
119
120 - def __getitem__(self, name):
121 """ 122 Return a query_param object for param named NAME. 123 """ 124 for param in self.params: 125 if param.name == name: 126 return param 127 128 raise KeyError("No pram named %s" % repr(name))
129
130 - def fill_in_defaults(self, request):
131 """ 132 Modify the REQUEST dict and fill in default values where needed. 133 """ 134 for param in self.params: 135 if not request.has_key(param.name): 136 request[param.name] = param.default_value
137
138 - def generic_sql_clauses(self, request, available_rows=None):
139 """ 140 Return a list contining an sql.orderby, sql.offset and an 141 sql.limit object corresponding to REQUEST. You may specify the 142 AVAILABLE_ROWS parameter to have the method range-check the 143 page param. 144 """ 145 # Make sure the request contains valid values to prevent SQL 146 # injection. 147 assert self.has_param("orderby") 148 assert self.has_param("orderdir") 149 assert self.has_param("page") 150 assert self.has_param("page_size") 151 152 self.fill_in_defaults(request) 153 self.validate_all(request) 154 155 # ORDER BY ... 156 if not (request.has_key("orderby") and request.has_key("orderdir")): 157 request["orderby"] = self["orderby"].default_value 158 request["orderdir"] = self["orderdir"].default_value 159 160 page = self["page"].get(request) 161 page = int(page) 162 163 page_size = self["page_size"].get(request) 164 page_size = int(page_size) 165 166 if available_rows is not None: 167 if (page+1) * page_size > available_rows: 168 raise ValueError("Page does not exist (%i)" % page) 169 170 return [ sql.orderby(str(request["orderby"]), 171 dir=str(request["orderdir"])), 172 sql.offset(page*page_size), 173 sql.limit(page_size), ]
174 175 176 177 178
179 -class query_param:
180 """ 181 A param for an sql query to the result_table. 182 """
183 - def __init__(self, name, default_value, available_values=()):
184 """ 185 @param name: Name of the param in the url 186 @param default_value: Just that. 187 @param available_values: A list of values that are allowed 188 for this param 189 """ 190 self.name = name 191 self.default_value = default_value 192 self.available_values = Set(map(str, available_values)) 193 194 if len(available_values) > 0: 195 if str(self.default_value) not in self.available_values: 196 raise ValueError( 197 "Query param value %s not among available values %s" % ( 198 repr(self.default_value), repr(self.available_values),))
199
200 - def validate(self, value):
201 """ 202 Check if VALUE allowed for this param. 203 """ 204 if len(self.available_values) and \ 205 str(value) not in self.available_values: 206 raise ValueError("Param value %s not in %s" % ( 207 repr(value), repr(self.available_values),))
208
209 - def get(self, request):
210 """ 211 Get the value of this param from REQUEST, validate it. 212 """ 213 value = request.get(self.name, self.default_value) 214 self.validate(value) 215 return value
216
217 -class page_param(query_param):
218 """ 219 Query param for the current page. Does not have an 220 available_values argument/ivar. 221 """
222 - def __init__(self, name, default_value=0):
223 query_param.__init__(self, name, default_value, ())
224
225 - def validate(self, value):
226 # The page value is always considered valid. If an illegal 227 # values is passe dby url spoofing, the worst thing that could 228 # happen is an empty result set. It is validated by 229 # generic_sql_clauses(), though. 230 return True
231 232 233 # Classes responsible for HTML creation of the table elements 234
235 -class _cell:
236 """ 237 Parent class for those classes responsible for HTML creation of 238 the table elements. 239 """
240 - def __init__(self, result_table, dbproperty):
241 self.result_table = result_table 242 self.dbproperty = dbproperty
243
244 - def __call__(self, base_url, request):
245 """ 246 Return the HTLM for this cell as a XIST DOM tree. 247 """ 248 raise NotImplementedError()
249
250 -class header(_cell):
251 """ 252 A header element for a result_table. 253 """
254 - def __init__(self, result_table, dbproperty, title=None):
255 """ 256 @param dbproperty: Dbproperty displayed in the column headed by 257 this header. May be None. 258 @param title: The title used by the header, defaults to the 259 dbproperty's title. 260 """ 261 _cell.__init__(self, result_table, dbproperty) 262 self._title = title 263 264 result_table.headers.append(self)
265
266 - def __call__(self, base_url, request):
267 """ 268 @returns: a cell (html.th()) element. 269 """ 270 return html.th(self.title())
271
272 - def title(self):
273 """ 274 Return a valid title as a string. 275 """ 276 if self._title is None: 277 if self.dbproperty is None: 278 return "" 279 else: 280 return self.dbproperty.title 281 else: 282 return self._title
283
284 -class sort_header(header):
285 """ 286 Header for a column that may be sorted. Provides a link, clicking 287 on which will sort the table according to this column. If this 288 column is the currently active column, the sort order will be 289 reversed. Whether the column is active is indicated throught the 290 class= attribute of the html.a() created: ''active'' or empty. 291 """
292 - def __init__(self, result_table, dbproperty, title=None):
293 header.__init__(self, result_table, dbproperty, title) 294 295 # Keep a list of columns that may be used for sorting 296 # in the result_table object. 297 self.result_table.order_by_columns.append( 298 self.dbproperty.attribute_name) 299 300 # Make sure all sortable columns may be mentioned in the 301 # orderby param. 302 self.result_table.params["orderby"].available_values.add( 303 self.dbproperty.attribute_name)
304
305 - def __call__(self, base_url, request):
306 current_orderby = request.get("orderby") 307 current_orderdir = request.get("orderdir") 308 309 if current_orderby == self.dbproperty.attribute_name: 310 cls = "active" 311 312 if lower(current_orderdir) == "asc": 313 orderdir = "desc" 314 else: 315 orderdir = "asc" 316 else: 317 cls = None 318 orderdir = "asc" 319 320 href = self.result_table.params.href( 321 base_url, self.result_table, request, 322 orderby = self.dbproperty.attribute_name, orderdir = orderdir) 323 324 return html.th(html.a(self.title(), href=href, class_=cls))
325
326 -class arbitrary_cell:
327 - class dummy_dbproperty:
328 - def __init__(self, attribute_name):
331
332 -class arbitrary_header(header, arbitrary_cell):
333 """ 334 An arbitrary_header belongs to an arbitrary column. They are 335 matched by their name (which takes the place of the dbproperty's 336 attribute_name). 337 """
338 - def __init__(self, result_table, name, title=None):
340
341 -class result_table_widget:
342 """ 343 A result_table_widget is a user interface element that belongs to 344 a result_table, as a pager or a widget that lets you choose the 345 number of items displayed at any time. This is a generic base class. 346 """
347 - def __init__(self):
348 pass
349
350 - def __call__(self, result_table, request, base_url):
351 return Frag()
352
353 -class pager(result_table_widget):
354 """ 355 A pager is a widget that lets you navigate in a paged result 356 display. 357 """ 358 pass
359 360 # The null_pager is a pager that does nothing 361 null_pager = pager 362
363 -class page_size_chooser(result_table_widget):
364 """ 365 A page_size_chooser lets you choose how many items you'd like to 366 see per result page. 367 """ 368 pass
369 370 # The null_page_size_chooser does nothing 371 null_page_size_chooser = page_size_chooser 372 408 409
410 -class andreas_pager(pager):
411 """ 412 Pager widget based on an idea by Andreas Junge of jungepartner, 413 Witten (Germany) <http://www.jungepartner.de>. 414 415 It's a smart way of prividing page links for datasets of up to 416 1000 or so records. 417 """
418 - def __init__(self):
419 pass
420
421 - def __call__1(self, result_table, request, base_url):
422 """ 423 Obsolete old way of getting the needed rows from the RDBMS. 424 """ 425 orderby = result_table.params["orderby"].get(request) 426 current_page = int(result_table.params["page"].get(request)) 427 all_rows = result_table.result.count_all() 428 page_size = int(result_table.params["page_size"].get(request)) 429 number_of_pages = ( all_rows / page_size ) 430 if all_rows % page_size > 0: number_of_pages += 1 431 432 page_info = [] 433 434 ret = html.div(class_="pager") 435 436 for a in range(number_of_pages): 437 first = a * page_size 438 if a != 0: first += 1 439 440 last = (a+1) * page_size 441 if last > all_rows-1: last = all_rows-1 442 443 left = self.fetch_row(result_table, first) 444 right = self.fetch_row(result_table, last) 445 446 left_info = self.format_value(left, orderby) 447 right_info = self.format_value(right, orderby) 448 449 if a == current_page: 450 cls = "active" 451 else: 452 cls = None 453 454 href = result_table.params.href(base_url, result_table, request, 455 page=a) 456 457 ret.append(html.a(left_info, chars.mdash(), right_info, 458 href=href, class_=cls), " ") 459 460 return ret
461 462
463 - def fetch_row(self, result_table, row_no):
464 """ 465 Fetch one row from the RDBMS, used to be called by __call1__(). 466 """ 467 result = result_table.result 468 ds = result.ds 469 dbclass = result.dbclass 470 471 select = copy.deepcopy(result.select) 472 select.modify(sql.offset(row_no)) 473 select.modify(sql.limit(1)) 474 result = ds.run_select(dbclass, select) 475 return result.next()
476
477 - def __call__(self, result_table, request, base_url):
478 """ 479 """ 480 # The complicated thing about this is that all the relevant 481 # primary keys are retrieved from the RDBMS, a subset needed 482 # to draw the pager is chosen and only those rows needed are 483 # SELECTed from the database. This takes into account that 484 # orm2 allows for multi column primary keys! 485 486 orderby_attribute = result_table.params["orderby"].get(request) 487 current_page = int(result_table.params["page"].get(request)) 488 all_rows = result_table.result.count_all() 489 page_size = int(result_table.params["page_size"].get(request)) 490 number_of_pages = all_rows / page_size 491 if all_rows % page_size > 0: number_of_pages += 1 492 dbclass = result_table.result.dbclass 493 494 dummy = dbclass() 495 primary_key_attributes = list(dummy.__primary_key__.attributes()) 496 primary_key_columns = list(dummy.__primary_key__.columns()) 497 498 where = None 499 orderby = None 500 for clause in result_table.result.select.clauses: 501 if isinstance(clause, sql.where): 502 where = clause 503 504 if isinstance(clause, sql.orderby): 505 orderby = clause 506 507 all_keys = self.get_all_primary_keys(result_table, 508 primary_key_columns, 509 where, orderby) 510 511 keys = [] 512 513 for a in range(number_of_pages): 514 first = a * page_size 515 516 last = ((a+1) * page_size) - 1 517 if last > all_rows-1: last = all_rows-1 518 519 keys.append(all_keys[first]) 520 keys.append(all_keys[last]) 521 522 obj_dict = self.fetch_rows(result_table, primary_key_attributes, 523 keys, orderby) 524 525 keys.reverse() 526 527 ret = html.div(class_="pager") 528 counter = 0 529 while keys: 530 left_key = keys.pop() 531 right_key = keys.pop() 532 533 left = obj_dict[left_key] 534 right = obj_dict[right_key] 535 536 left_info = self.format_value(left, orderby_attribute) 537 right_info = self.format_value(right, orderby_attribute) 538 539 if counter == current_page: 540 cls = "active" 541 else: 542 cls = None 543 544 href = result_table.params.href(base_url, 545 result_table, request, 546 page=counter) 547 548 ret.append(html.a(left_info, chars.mdash(), right_info, 549 href=href, class_=cls)) 550 if len(keys) >= 2: ret.append(html.span(" | ", class_="bar")) 551 552 counter += 1 553 554 return ret
555
556 - def get_all_primary_keys(self, result_table, 557 primary_key_columns, 558 where, orderby):
559 """ 560 Return a list of all relevant primary keys. 561 562 @returns: A list of tuples. 563 """ 564 result = result_table.result 565 ds = result.ds 566 dbclass = result_table.result.dbclass 567 568 cursor = ds.execute(sql.select(primary_key_columns, 569 dbclass.__relation__, 570 where, orderby)) 571 572 return cursor.fetchall()
573 574
575 - def fetch_rows(self, result_table, primary_key_attributes, 576 primary_key_data, orderby):
577 """ 578 Fetch those rows indicated by the PRIMARY_KEY_DATA. 579 580 @param primary_key_data: A list of tuples containing primary 581 keys, as returned by get_all_primary_keys() above. 582 583 @returns: A dict as {(pkey,): dbobj} 584 """ 585 result = result_table.result 586 ds = result.ds 587 dbclass = result.dbclass 588 589 # The WHERE is (column = value AND column = value) OR 590 # (column = value AND column = value) OR ... 591 # for each and every key selected from the list. 592 593 conditions = [] 594 for key_data in primary_key_data: 595 conditions.append("(") 596 conditions.append(self.row_condition(dbclass, 597 primary_key_attributes, 598 key_data)) 599 conditions.append(")") 600 conditions.append(" OR ") 601 602 conditions.pop() # Remove superflous OR 603 604 result = ds.select(dbclass, sql.where(conditions), orderby) 605 606 ret = {} 607 for dbobj in result: 608 ret[dbobj.__primary_key__.values()] = dbobj 609 610 return ret
611
612 - def row_condition(self, dbclass, key_attributes, key_data):
613 """ 614 Return a list that goes into an sql.where() to select the 615 particular row referenced by key_data. 616 """ 617 conditions = [] 618 619 for attribute, data in zip(key_attributes, key_data): 620 column = attribute.column 621 literal = attribute.sql_literal_class(data) 622 623 conditions.append(column) 624 conditions.append(" = ") 625 conditions.append(literal) 626 conditions.append(" AND ") 627 628 conditions.pop() # Remove superflous AND 629 630 return conditions
631 632
633 - def format_value(self, dbobj):
634 """ 635 Format a single value that goes laft and right of the -- in 636 the page links. 637 """ 638 return getattr(dbobj, attribute_name)
639
640 -class column(_cell):
641 """ 642 Maybe this should be call 'data cell' or so, it's a cell in a 643 column. 644 """
645 - def __init__(self, result_table, dbproperty):
646 _cell.__init__(self, result_table, dbproperty) 647 result_table.columns.append(self)
648
649 - def __call__(self, dbobj, base_url, request):
650 return html.td(self.dbproperty.__get__(dbobj))
651
652 -class arbitrary_column(column, arbitrary_cell):
653 """ 654 An arbitrary_column belongs to an arbitrary header. They are 655 matched by their `name`, which takes the place of the 656 dbproperty.attribute_name. You must overload the __call__() 657 method for this to do anything usefull, in which case, you can 658 inset arbitrary content into your table, hence the name. 659 """
660 - def __init__(self, result_table, name):
662
663 - def __call__(self, dbobj, base_url, request):
664 return Frag()
665
666 -class result_table(xist_view):
667 """ 668 A result table consists of a number of header objects and a 669 corresponding set of column objects. The header objects determine 670 which actual database columns, that is which dbproperties, are used 671 in the column and which are used for sorting (see sort_header class). 672 """ 673 params = query_params(query_param("page_size", 20, (20, 50, 100,)), 674 page_param("page", 0)) 675 676 pager = null_pager() 677 page_size_chooser = null_page_size_chooser() 678 679
680 - def __init__(self, result=None):
681 xist_view.__init__(self, result=result) 682 683 self.headers = [] 684 self.columns = [] 685 self.order_by_columns = [] # List of columns that may be used in
686 # ORDER BY clauses. 687 688
689 - def __call__(self, request={}, base_url=None, class_="listing"):
690 ret = html.div() 691 692 ret.append(self.table(request, base_url, class_=class_)) 693 ret.append(self.pager(self, request, base_url)) 694 ret.append(self.page_size_chooser(self, request, base_url)) 695 696 return ret
697 698
699 - def table(self, request={}, base_url=None, class_="listing"):
700 # Make sure all params are present, fill in default values if not. 701 self.params.fill_in_defaults(request) 702 703 # Make sure there is a column instance for every header instance 704 # and that matching indeces in both list refer to matching 705 # dbpropertied. 706 column_by_dbproperty = {} 707 for col in self.columns: 708 column_by_dbproperty[col.dbproperty.attribute_name] = col 709 710 columns = [] 711 for header in self.headers: 712 col = column_by_dbproperty.get(header.dbproperty.attribute_name, 713 None) 714 if col is None: 715 columns.append(column(self, header.dbproperty)) 716 else: 717 columns.append(col) 718 719 self.columns = columns 720 721 # The head 722 tr = html.tr() 723 for header in self.headers: 724 tr.append(header(base_url, request)) 725 726 thead = html.thead(tr) 727 728 # The body 729 tbody = html.tbody() 730 731 for counter, dbobj in enumerate(self.result): 732 tbody.append(self.tr(dbobj, base_url, request, 733 ("even", "odd")[counter%2])) 734 735 736 # Good. Now build the table 737 return html.table(thead, tbody, class_=class_, cellspacing=0)
738
739 - def tr(self, dbobj, base_url, request, class_):
740 tr = html.tr(class_=class_) 741 742 for col in self.columns: 743 tr.append(col(dbobj, base_url, request)) 744 745 return tr
746