1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
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 """
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
92
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
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
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
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
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
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
146
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
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
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
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
227
228
229
230 return True
231
232
233
234
236 """
237 Parent class for those classes responsible for HTML creation of
238 the table elements.
239 """
240 - def __init__(self, result_table, dbproperty):
243
245 """
246 Return the HTLM for this cell as a XIST DOM tree.
247 """
248 raise NotImplementedError()
249
251 """
252 A header element for a result_table.
253 """
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
267 """
268 @returns: a cell (html.th()) element.
269 """
270 return html.th(self.title())
271
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
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 """
304
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
331
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 """
340
352
354 """
355 A pager is a widget that lets you navigate in a paged result
356 display.
357 """
358 pass
359
360
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
371 null_page_size_chooser = page_size_chooser
372
373 -class link_page_size_chooser(page_size_chooser):
374 '''
375 This is the most simple page_size_chooser. It depends on the
376 page_size param (and its presence among the query_params!) and it
377 will create one link for each page size, the current one having
378 the class="active". These will be in a <div class="page-size-chooser">
379 The acailable page sizes will be taken from the page_size_param`s
380 available_values attribute.
381 '''
382 - def __init__(self):
383 pass
384
385 - def __call__(self, result_table, request, base_url):
386 ret = html.div(class_="page-size-chooser")
387
388 param = result_table.params["page_size"]
389 current_page_size = int(param.get(request))
390
391 available_sizes = list(param.available_values)
392 available_sizes = map(int, available_sizes)
393 available_sizes.sort()
394
395 for size in available_sizes:
396 href = result_table.params.href(base_url, result_table, request,
397 page_size=size, page=0)
398
399 active = (int(size) == current_page_size)
400 cls = {True: "active", False: None}[active]
401
402 ret.append(html.a(size, href=href, class_=cls))
403 ret.append(html.span(" | ", class_="bar"))
404
405 del ret[-1]
406
407 return ret
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
481
482
483
484
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
590
591
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()
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()
629
630 return conditions
631
632
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
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):
648
649 - def __call__(self, dbobj, base_url, request):
650 return html.td(self.dbproperty.__get__(dbobj))
651
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
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
686
687
688
689 - def __call__(self, request={}, base_url=None, class_="listing"):
697
698
699 - def table(self, request={}, base_url=None, class_="listing"):
700
701 self.params.fill_in_defaults(request)
702
703
704
705
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
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
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
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