Package web2py :: Package gluon :: Module sqlhtml
[hide private]
[frames] | no frames]

Source Code for Module web2py.gluon.sqlhtml

   1  #!/usr/bin/env python 
   2  # -*- coding: utf-8 -*- 
   3   
   4  """ 
   5  This file is part of the web2py Web Framework 
   6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
   7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
   8   
   9  Holds: 
  10   
  11  - SQLFORM: provide a form for a table (with/without record) 
  12  - SQLTABLE: provides a table for a set of records 
  13  - form_factory: provides a SQLFORM for an non-db backed table 
  14   
  15  """ 
  16   
  17  from http import HTTP 
  18  from html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT 
  19  from html import FORM, INPUT, LABEL, OPTION, SELECT 
  20  from html import TABLE, THEAD, TBODY, TR, TD, TH 
  21  from html import URL 
  22  from dal import DAL, Table, Row, CALLABLETYPES, smart_query 
  23  from storage import Storage 
  24  from utils import md5_hash 
  25  from validators import IS_EMPTY_OR 
  26   
  27  import urllib 
  28  import re 
  29  import cStringIO 
  30   
  31  table_field = re.compile('[\w_]+\.[\w_]+') 
  32  widget_class = re.compile('^\w*') 
  33   
34 -def represent(field,value,record):
35 f = field.represent 36 if not callable(f): 37 return str(value) 38 n = f.func_code.co_argcount-len(f.func_defaults or []) 39 if n==1: 40 return f(value) 41 elif n==2: 42 return f(value,record) 43 else: 44 raise RuntimeError, "field representation must take 1 or 2 args"
45
46 -def safe_int(x):
47 try: 48 return int(x) 49 except ValueError: 50 return 0
51
52 -def safe_float(x):
53 try: 54 return float(x) 55 except ValueError: 56 return 0
57
58 -class FormWidget(object):
59 """ 60 helper for SQLFORM to generate form input fields (widget), 61 related to the fieldtype 62 """ 63 64 @staticmethod
65 - def _attributes(field, widget_attributes, **attributes):
66 """ 67 helper to build a common set of attributes 68 69 :param field: the field involved, some attributes are derived from this 70 :param widget_attributes: widget related attributes 71 :param attributes: any other supplied attributes 72 """ 73 attr = dict( 74 _id = '%s_%s' % (field._tablename, field.name), 75 _class = widget_class.match(str(field.type)).group(), 76 _name = field.name, 77 requires = field.requires, 78 ) 79 attr.update(widget_attributes) 80 attr.update(attributes) 81 return attr
82 83 @staticmethod
84 - def widget(field, value, **attributes):
85 """ 86 generates the widget for the field. 87 88 When serialized, will provide an INPUT tag: 89 90 - id = tablename_fieldname 91 - class = field.type 92 - name = fieldname 93 94 :param field: the field needing the widget 95 :param value: value 96 :param attributes: any other attributes to be applied 97 """ 98 99 raise NotImplementedError
100
101 -class StringWidget(FormWidget):
102 103 @staticmethod
104 - def widget(field, value, **attributes):
105 """ 106 generates an INPUT text tag. 107 108 see also: :meth:`FormWidget.widget` 109 """ 110 111 default = dict( 112 _type = 'text', 113 value = (not value is None and str(value)) or '', 114 ) 115 attr = StringWidget._attributes(field, default, **attributes) 116 117 return INPUT(**attr)
118 119
120 -class IntegerWidget(StringWidget):
121 122 pass
123 124
125 -class DoubleWidget(StringWidget):
126 127 pass
128 129
130 -class DecimalWidget(StringWidget):
131 132 pass
133 134
135 -class TimeWidget(StringWidget):
136 137 pass
138 139
140 -class DateWidget(StringWidget):
141 142 pass
143 144
145 -class DatetimeWidget(StringWidget):
146 147 pass
148 149
150 -class TextWidget(FormWidget):
151 152 @staticmethod
153 - def widget(field, value, **attributes):
154 """ 155 generates a TEXTAREA tag. 156 157 see also: :meth:`FormWidget.widget` 158 """ 159 160 default = dict( 161 value = value, 162 ) 163 attr = TextWidget._attributes(field, default, **attributes) 164 165 return TEXTAREA(**attr)
166 167
168 -class BooleanWidget(FormWidget):
169 170 @staticmethod
171 - def widget(field, value, **attributes):
172 """ 173 generates an INPUT checkbox tag. 174 175 see also: :meth:`FormWidget.widget` 176 """ 177 178 default=dict( 179 _type='checkbox', 180 value=value, 181 ) 182 attr = BooleanWidget._attributes(field, default, **attributes) 183 184 return INPUT(**attr)
185 186
187 -class OptionsWidget(FormWidget):
188 189 @staticmethod
190 - def has_options(field):
191 """ 192 checks if the field has selectable options 193 194 :param field: the field needing checking 195 :returns: True if the field has options 196 """ 197 198 return hasattr(field.requires, 'options')
199 200 @staticmethod
201 - def widget(field, value, **attributes):
202 """ 203 generates a SELECT tag, including OPTIONs (only 1 option allowed) 204 205 see also: :meth:`FormWidget.widget` 206 """ 207 default = dict( 208 value=value, 209 ) 210 attr = OptionsWidget._attributes(field, default, **attributes) 211 212 requires = field.requires 213 if not isinstance(requires, (list, tuple)): 214 requires = [requires] 215 if requires: 216 if hasattr(requires[0], 'options'): 217 options = requires[0].options() 218 else: 219 raise SyntaxError, 'widget cannot determine options of %s' \ 220 % field 221 opts = [OPTION(v, _value=k) for (k, v) in options] 222 223 return SELECT(*opts, **attr)
224
225 -class ListWidget(StringWidget):
226 @staticmethod
227 - def widget(field,value,**attributes):
228 _id = '%s_%s' % (field._tablename, field.name) 229 _name = field.name 230 if field.type=='list:integer': _class = 'integer' 231 else: _class = 'string' 232 items=[LI(INPUT(_id=_id,_class=_class,_name=_name,value=v,hideerror=True)) \ 233 for v in value or ['']] 234 script=SCRIPT(""" 235 // from http://refactormycode.com/codes/694-expanding-input-list-using-jquery 236 (function(){ 237 jQuery.fn.grow_input = function() { 238 return this.each(function() { 239 var ul = this; 240 jQuery(ul).find(":text").after('<a href="javascript:void(0)>+</a>').keypress(function (e) { return (e.which == 13) ? pe(ul) : true; }).next().click(function(){ pe(ul) }); 241 }); 242 }; 243 function pe(ul) { 244 var new_line = ml(ul); 245 rel(ul); 246 new_line.appendTo(ul); 247 new_line.find(":text").focus(); 248 return false; 249 } 250 function ml(ul) { 251 var line = jQuery(ul).find("li:first").clone(true); 252 line.find(':text').val(''); 253 return line; 254 } 255 function rel(ul) { 256 jQuery(ul).find("li").each(function() { 257 var trimmed = jQuery.trim(jQuery(this.firstChild).val()); 258 if (trimmed=='') jQuery(this).remove(); else jQuery(this.firstChild).val(trimmed); 259 }); 260 } 261 })(); 262 jQuery(document).ready(function(){jQuery('#%s_grow_input').grow_input();}); 263 """ % _id) 264 attributes['_id']=_id+'_grow_input' 265 return TAG[''](UL(*items,**attributes),script)
266 267
268 -class MultipleOptionsWidget(OptionsWidget):
269 270 @staticmethod
271 - def widget(field, value, size=5, **attributes):
272 """ 273 generates a SELECT tag, including OPTIONs (multiple options allowed) 274 275 see also: :meth:`FormWidget.widget` 276 277 :param size: optional param (default=5) to indicate how many rows must 278 be shown 279 """ 280 281 attributes.update(dict(_size=size, _multiple=True)) 282 283 return OptionsWidget.widget(field, value, **attributes)
284 285
286 -class RadioWidget(OptionsWidget):
287 288 @staticmethod
289 - def widget(field, value, **attributes):
290 """ 291 generates a TABLE tag, including INPUT radios (only 1 option allowed) 292 293 see also: :meth:`FormWidget.widget` 294 """ 295 296 attr = RadioWidget._attributes(field, {}, **attributes) 297 attr['_class'] = attr.get('_class','web2py_radiowidget') 298 299 requires = field.requires 300 if not isinstance(requires, (list, tuple)): 301 requires = [requires] 302 if requires: 303 if hasattr(requires[0], 'options'): 304 options = requires[0].options() 305 else: 306 raise SyntaxError, 'widget cannot determine options of %s' \ 307 % field 308 options = [(k, v) for k, v in options if str(v)] 309 opts = [] 310 cols = attributes.get('cols',1) 311 totals = len(options) 312 mods = totals%cols 313 rows = totals/cols 314 if mods: 315 rows += 1 316 317 #widget style 318 wrappers = dict( 319 table=(TABLE,TR,TD), 320 ul=(DIV,UL,LI), 321 divs=(CAT,DIV,DIV) 322 ) 323 parent, child, inner = wrappers[attributes.get('style','table')] 324 325 for r_index in range(rows): 326 tds = [] 327 for k, v in options[r_index*cols:(r_index+1)*cols]: 328 checked={'_checked':'checked'} if k==value else {} 329 tds.append(inner(INPUT(_type='radio', 330 _id='%s%s' % (field.name,k), 331 _name=field.name, 332 requires=attr.get('requires',None), 333 hideerror=True, _value=k, 334 value=value, 335 **checked), 336 LABEL(v,_for='%s%s' % (field.name,k)))) 337 opts.append(child(tds)) 338 339 if opts: 340 opts[-1][0][0]['hideerror'] = False 341 return parent(*opts, **attr)
342 343
344 -class CheckboxesWidget(OptionsWidget):
345 346 @staticmethod
347 - def widget(field, value, **attributes):
348 """ 349 generates a TABLE tag, including INPUT checkboxes (multiple allowed) 350 351 see also: :meth:`FormWidget.widget` 352 """ 353 354 # was values = re.compile('[\w\-:]+').findall(str(value)) 355 if isinstance(value, (list, tuple)): 356 values = [str(v) for v in value] 357 else: 358 values = [str(value)] 359 360 attr = CheckboxesWidget._attributes(field, {}, **attributes) 361 attr['_class'] = attr.get('_class','web2py_checkboxeswidget') 362 363 requires = field.requires 364 if not isinstance(requires, (list, tuple)): 365 requires = [requires] 366 if requires: 367 if hasattr(requires[0], 'options'): 368 options = requires[0].options() 369 else: 370 raise SyntaxError, 'widget cannot determine options of %s' \ 371 % field 372 373 options = [(k, v) for k, v in options if k != ''] 374 opts = [] 375 cols = attributes.get('cols', 1) 376 totals = len(options) 377 mods = totals % cols 378 rows = totals / cols 379 if mods: 380 rows += 1 381 382 #widget style 383 wrappers = dict( 384 table=(TABLE,TR,TD), 385 ul=(DIV,UL,LI), 386 divs=(CAT,DIV,DIV) 387 ) 388 parent, child, inner = wrappers[attributes.get('style','table')] 389 390 for r_index in range(rows): 391 tds = [] 392 for k, v in options[r_index*cols:(r_index+1)*cols]: 393 if k in values: 394 r_value = k 395 else: 396 r_value = [] 397 tds.append(inner(INPUT(_type='checkbox', 398 _id='%s%s' % (field.name,k), 399 _name=field.name, 400 requires=attr.get('requires', None), 401 hideerror=True, _value=k, 402 value=r_value), 403 LABEL(v,_for='%s%s' % (field.name,k)))) 404 opts.append(child(tds)) 405 406 if opts: 407 opts[-1][0][0]['hideerror'] = False 408 return parent(*opts, **attr)
409 410
411 -class PasswordWidget(FormWidget):
412 413 DEFAULT_PASSWORD_DISPLAY = 8*('*') 414 415 @staticmethod
416 - def widget(field, value, **attributes):
417 """ 418 generates a INPUT password tag. 419 If a value is present it will be shown as a number of '*', not related 420 to the length of the actual value. 421 422 see also: :meth:`FormWidget.widget` 423 """ 424 425 default=dict( 426 _type='password', 427 _value=(value and PasswordWidget.DEFAULT_PASSWORD_DISPLAY) or '', 428 ) 429 attr = PasswordWidget._attributes(field, default, **attributes) 430 431 return INPUT(**attr)
432 433
434 -class UploadWidget(FormWidget):
435 436 DEFAULT_WIDTH = '150px' 437 ID_DELETE_SUFFIX = '__delete' 438 GENERIC_DESCRIPTION = 'file' 439 DELETE_FILE = 'delete' 440 441 @staticmethod
442 - def widget(field, value, download_url=None, **attributes):
443 """ 444 generates a INPUT file tag. 445 446 Optionally provides an A link to the file, including a checkbox so 447 the file can be deleted. 448 All is wrapped in a DIV. 449 450 see also: :meth:`FormWidget.widget` 451 452 :param download_url: Optional URL to link to the file (default = None) 453 """ 454 455 default=dict( 456 _type='file', 457 ) 458 attr = UploadWidget._attributes(field, default, **attributes) 459 460 inp = INPUT(**attr) 461 462 if download_url and value: 463 if callable(download_url): 464 url = download_url(value) 465 else: 466 url = download_url + '/' + value 467 (br, image) = ('', '') 468 if UploadWidget.is_image(value): 469 br = BR() 470 image = IMG(_src = url, _width = UploadWidget.DEFAULT_WIDTH) 471 472 requires = attr["requires"] 473 if requires == [] or isinstance(requires, IS_EMPTY_OR): 474 inp = DIV(inp, '[', 475 A(UploadWidget.GENERIC_DESCRIPTION, _href = url), 476 '|', 477 INPUT(_type='checkbox', 478 _name=field.name + UploadWidget.ID_DELETE_SUFFIX, 479 _id=field.name + UploadWidget.ID_DELETE_SUFFIX), 480 LABEL(UploadWidget.DELETE_FILE, 481 _for=field.name + UploadWidget.ID_DELETE_SUFFIX), 482 ']', br, image) 483 else: 484 inp = DIV(inp, '[', 485 A(UploadWidget.GENERIC_DESCRIPTION, _href = url), 486 ']', br, image) 487 return inp
488 489 @staticmethod
490 - def represent(field, value, download_url=None):
491 """ 492 how to represent the file: 493 494 - with download url and if it is an image: <A href=...><IMG ...></A> 495 - otherwise with download url: <A href=...>file</A> 496 - otherwise: file 497 498 :param field: the field 499 :param value: the field value 500 :param download_url: url for the file download (default = None) 501 """ 502 503 inp = UploadWidget.GENERIC_DESCRIPTION 504 505 if download_url and value: 506 if callable(download_url): 507 url = download_url(value) 508 else: 509 url = download_url + '/' + value 510 if UploadWidget.is_image(value): 511 inp = IMG(_src = url, _width = UploadWidget.DEFAULT_WIDTH) 512 inp = A(inp, _href = url) 513 514 return inp
515 516 @staticmethod
517 - def is_image(value):
518 """ 519 Tries to check if the filename provided references to an image 520 521 Checking is based on filename extension. Currently recognized: 522 gif, png, jp(e)g, bmp 523 524 :param value: filename 525 """ 526 527 extension = value.split('.')[-1].lower() 528 if extension in ['gif', 'png', 'jpg', 'jpeg', 'bmp']: 529 return True 530 return False
531 532
533 -class AutocompleteWidget(object):
534
535 - def __init__(self, request, field, id_field=None, db=None, 536 orderby=None, limitby=(0,10), 537 keyword='_autocomplete_%(fieldname)s', 538 min_length=2):
539 self.request = request 540 self.keyword = keyword % dict(fieldname=field.name) 541 self.db = db or field._db 542 self.orderby = orderby 543 self.limitby = limitby 544 self.min_length = min_length 545 self.fields=[field] 546 if id_field: 547 self.is_reference = True 548 self.fields.append(id_field) 549 else: 550 self.is_reference = False 551 if hasattr(request,'application'): 552 self.url = URL(args=request.args) 553 self.callback() 554 else: 555 self.url = request
556 - def callback(self):
557 if self.keyword in self.request.vars: 558 field = self.fields[0] 559 rows = self.db(field.like(self.request.vars[self.keyword]+'%'))\ 560 .select(orderby=self.orderby,limitby=self.limitby,*self.fields) 561 if rows: 562 if self.is_reference: 563 id_field = self.fields[1] 564 raise HTTP(200,SELECT(_id=self.keyword,_class='autocomplete', 565 _size=len(rows),_multiple=(len(rows)==1), 566 *[OPTION(s[field.name],_value=s[id_field.name], 567 _selected=(k==0)) \ 568 for k,s in enumerate(rows)]).xml()) 569 else: 570 raise HTTP(200,SELECT(_id=self.keyword,_class='autocomplete', 571 _size=len(rows),_multiple=(len(rows)==1), 572 *[OPTION(s[field.name], 573 _selected=(k==0)) \ 574 for k,s in enumerate(rows)]).xml()) 575 else: 576 577 raise HTTP(200,'')
578 - def __call__(self,field,value,**attributes):
579 default = dict( 580 _type = 'text', 581 value = (not value is None and str(value)) or '', 582 ) 583 attr = StringWidget._attributes(field, default, **attributes) 584 div_id = self.keyword+'_div' 585 attr['_autocomplete']='off' 586 if self.is_reference: 587 key2 = self.keyword+'_aux' 588 key3 = self.keyword+'_auto' 589 attr['_class']='string' 590 name = attr['_name'] 591 if 'requires' in attr: del attr['requires'] 592 attr['_name'] = key2 593 value = attr['value'] 594 record = self.db(self.fields[1]==value).select(self.fields[0]).first() 595 attr['value'] = record and record[self.fields[0].name] 596 attr['_onblur']="jQuery('#%(div_id)s').delay(3000).fadeOut('slow');" % \ 597 dict(div_id=div_id,u='F'+self.keyword) 598 attr['_onkeyup'] = "jQuery('#%(key3)s').val('');var e=event.which?event.which:event.keyCode; function %(u)s(){jQuery('#%(id)s').val(jQuery('#%(key)s :selected').text());jQuery('#%(key3)s').val(jQuery('#%(key)s').val())}; if(e==39) %(u)s(); else if(e==40) {if(jQuery('#%(key)s option:selected').next().length)jQuery('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); %(u)s();} else if(e==38) {if(jQuery('#%(key)s option:selected').prev().length)jQuery('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); %(u)s();} else if(jQuery('#%(id)s').val().length>=%(min_length)s) jQuery.get('%(url)s?%(key)s='+escape(jQuery('#%(id)s').val()),function(data){if(data=='')jQuery('#%(key3)s').val('');else{jQuery('#%(id)s').next('.error').hide();jQuery('#%(div_id)s').html(data).show().focus();jQuery('#%(div_id)s select').css('width',jQuery('#%(id)s').css('width'));jQuery('#%(key3)s').val(jQuery('#%(key)s').val());jQuery('#%(key)s').change(%(u)s);jQuery('#%(key)s').click(%(u)s);};}); else jQuery('#%(div_id)s').fadeOut('slow');" % \ 599 dict(url=self.url,min_length=self.min_length, 600 key=self.keyword,id=attr['_id'],key2=key2,key3=key3, 601 name=name,div_id=div_id,u='F'+self.keyword) 602 if self.min_length==0: 603 attr['_onfocus'] = attr['_onkeyup'] 604 return TAG[''](INPUT(**attr),INPUT(_type='hidden',_id=key3,_value=value, 605 _name=name,requires=field.requires), 606 DIV(_id=div_id,_style='position:absolute;')) 607 else: 608 attr['_name']=field.name 609 attr['_onblur']="jQuery('#%(div_id)s').delay(3000).fadeOut('slow');" % \ 610 dict(div_id=div_id,u='F'+self.keyword) 611 attr['_onkeyup'] = "var e=event.which?event.which:event.keyCode; function %(u)s(){jQuery('#%(id)s').val(jQuery('#%(key)s').val())}; if(e==39) %(u)s(); else if(e==40) {if(jQuery('#%(key)s option:selected').next().length)jQuery('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); %(u)s();} else if(e==38) {if(jQuery('#%(key)s option:selected').prev().length)jQuery('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); %(u)s();} else if(jQuery('#%(id)s').val().length>=%(min_length)s) jQuery.get('%(url)s?%(key)s='+escape(jQuery('#%(id)s').val()),function(data){jQuery('#%(id)s').next('.error').hide();jQuery('#%(div_id)s').html(data).show().focus();jQuery('#%(div_id)s select').css('width',jQuery('#%(id)s').css('width'));jQuery('#%(key)s').change(%(u)s);jQuery('#%(key)s').click(%(u)s);}); else jQuery('#%(div_id)s').fadeOut('slow');" % \ 612 dict(url=self.url,min_length=self.min_length, 613 key=self.keyword,id=attr['_id'],div_id=div_id,u='F'+self.keyword) 614 if self.min_length==0: 615 attr['_onfocus'] = attr['_onkeyup'] 616 return TAG[''](INPUT(**attr),DIV(_id=div_id,_style='position:absolute;'))
617 618
619 -class SQLFORM(FORM):
620 621 """ 622 SQLFORM is used to map a table (and a current record) into an HTML form 623 624 given a SQLTable stored in db.table 625 626 generates an insert form:: 627 628 SQLFORM(db.table) 629 630 generates an update form:: 631 632 record=db.table[some_id] 633 SQLFORM(db.table, record) 634 635 generates an update with a delete button:: 636 637 SQLFORM(db.table, record, deletable=True) 638 639 if record is an int:: 640 641 record=db.table[record] 642 643 optional arguments: 644 645 :param fields: a list of fields that should be placed in the form, 646 default is all. 647 :param labels: a dictionary with labels for each field, keys are the field 648 names. 649 :param col3: a dictionary with content for an optional third column 650 (right of each field). keys are field names. 651 :param linkto: the URL of a controller/function to access referencedby 652 records 653 see controller appadmin.py for examples 654 :param upload: the URL of a controller/function to download an uploaded file 655 see controller appadmin.py for examples 656 657 any named optional attribute is passed to the <form> tag 658 for example _class, _id, _style, _action, _method, etc. 659 660 """ 661 662 # usability improvements proposal by fpp - 4 May 2008 : 663 # - correct labels (for points to field id, not field name) 664 # - add label for delete checkbox 665 # - add translatable label for record ID 666 # - add third column to right of fields, populated from the col3 dict 667 668 widgets = Storage(dict( 669 string = StringWidget, 670 text = TextWidget, 671 password = PasswordWidget, 672 integer = IntegerWidget, 673 double = DoubleWidget, 674 decimal = DecimalWidget, 675 time = TimeWidget, 676 date = DateWidget, 677 datetime = DatetimeWidget, 678 upload = UploadWidget, 679 boolean = BooleanWidget, 680 blob = None, 681 options = OptionsWidget, 682 multiple = MultipleOptionsWidget, 683 radio = RadioWidget, 684 checkboxes = CheckboxesWidget, 685 autocomplete = AutocompleteWidget, 686 list = ListWidget, 687 )) 688 689 FIELDNAME_REQUEST_DELETE = 'delete_this_record' 690 FIELDKEY_DELETE_RECORD = 'delete_record' 691 ID_LABEL_SUFFIX = '__label' 692 ID_ROW_SUFFIX = '__row' 693
694 - def __init__( 695 self, 696 table, 697 record = None, 698 deletable = False, 699 linkto = None, 700 upload = None, 701 fields = None, 702 labels = None, 703 col3 = {}, 704 submit_button = 'Submit', 705 delete_label = 'Check to delete:', 706 showid = True, 707 readonly = False, 708 comments = True, 709 keepopts = [], 710 ignore_rw = False, 711 record_id = None, 712 formstyle = 'table3cols', 713 buttons = ['submit'], 714 separator = ': ', 715 **attributes 716 ):
717 """ 718 SQLFORM(db.table, 719 record=None, 720 fields=['name'], 721 labels={'name': 'Your name'}, 722 linkto=URL(f='table/db/') 723 """ 724 725 self.ignore_rw = ignore_rw 726 self.formstyle = formstyle 727 nbsp = XML('&nbsp;') # Firefox2 does not display fields with blanks 728 FORM.__init__(self, *[], **attributes) 729 ofields = fields 730 keyed = hasattr(table,'_primarykey') 731 732 # if no fields are provided, build it from the provided table 733 # will only use writable or readable fields, unless forced to ignore 734 if fields is None: 735 fields = [f.name for f in table if (ignore_rw or f.writable or f.readable) and not f.compute] 736 self.fields = fields 737 738 # make sure we have an id 739 if self.fields[0] != table.fields[0] and \ 740 isinstance(table,Table) and not keyed: 741 self.fields.insert(0, table.fields[0]) 742 743 self.table = table 744 745 # try to retrieve the indicated record using its id 746 # otherwise ignore it 747 if record and isinstance(record, (int, long, str, unicode)): 748 if not str(record).isdigit(): 749 raise HTTP(404, "Object not found") 750 record = table._db(table._id == record).select().first() 751 if not record: 752 raise HTTP(404, "Object not found") 753 self.record = record 754 755 self.record_id = record_id 756 if keyed: 757 if record: 758 self.record_id = dict([(k,record[k]) for k in table._primarykey]) 759 else: 760 self.record_id = dict([(k,None) for k in table._primarykey]) 761 self.field_parent = {} 762 xfields = [] 763 self.fields = fields 764 self.custom = Storage() 765 self.custom.dspval = Storage() 766 self.custom.inpval = Storage() 767 self.custom.label = Storage() 768 self.custom.comment = Storage() 769 self.custom.widget = Storage() 770 self.custom.linkto = Storage() 771 772 sep = separator or '' 773 774 for fieldname in self.fields: 775 if fieldname.find('.') >= 0: 776 continue 777 778 field = self.table[fieldname] 779 comment = None 780 781 if comments: 782 comment = col3.get(fieldname, field.comment) 783 if comment is None: 784 comment = '' 785 self.custom.comment[fieldname] = comment 786 787 if not labels is None and fieldname in labels: 788 label = labels[fieldname] 789 else: 790 label = field.label 791 self.custom.label[fieldname] = label 792 793 field_id = '%s_%s' % (table._tablename, fieldname) 794 795 label = LABEL(label, label and sep, _for=field_id, 796 _id=field_id+SQLFORM.ID_LABEL_SUFFIX) 797 798 row_id = field_id+SQLFORM.ID_ROW_SUFFIX 799 if field.type == 'id': 800 self.custom.dspval.id = nbsp 801 self.custom.inpval.id = '' 802 widget = '' 803 if record: 804 if showid and 'id' in fields and field.readable: 805 v = record['id'] 806 widget = SPAN(v, _id=field_id) 807 self.custom.dspval.id = str(v) 808 xfields.append((row_id,label, widget,comment)) 809 self.record_id = str(record['id']) 810 self.custom.widget.id = widget 811 continue 812 813 if readonly and not ignore_rw and not field.readable: 814 continue 815 816 if record: 817 default = record[fieldname] 818 else: 819 default = field.default 820 if isinstance(default,CALLABLETYPES): 821 default=default() 822 823 cond = readonly or \ 824 (not ignore_rw and not field.writable and field.readable) 825 826 if default and not cond: 827 default = field.formatter(default) 828 dspval = default 829 inpval = default 830 831 if cond: 832 833 # ## if field.represent is available else 834 # ## ignore blob and preview uploaded images 835 # ## format everything else 836 837 if field.represent: 838 inp = represent(field,default,record) 839 elif field.type in ['blob']: 840 continue 841 elif field.type == 'upload': 842 inp = UploadWidget.represent(field, default, upload) 843 elif field.type == 'boolean': 844 inp = self.widgets.boolean.widget(field, default, _disabled=True) 845 else: 846 inp = field.formatter(default) 847 elif field.type == 'upload': 848 if hasattr(field, 'widget') and field.widget: 849 inp = field.widget(field, default, upload) 850 else: 851 inp = self.widgets.upload.widget(field, default, upload) 852 elif hasattr(field, 'widget') and field.widget: 853 inp = field.widget(field, default) 854 elif field.type == 'boolean': 855 inp = self.widgets.boolean.widget(field, default) 856 if default: 857 inpval = 'checked' 858 else: 859 inpval = '' 860 elif OptionsWidget.has_options(field): 861 if not field.requires.multiple: 862 inp = self.widgets.options.widget(field, default) 863 else: 864 inp = self.widgets.multiple.widget(field, default) 865 if fieldname in keepopts: 866 inpval = TAG[''](*inp.components) 867 elif field.type.startswith('list:'): 868 inp = self.widgets.list.widget(field,default) 869 elif field.type == 'text': 870 inp = self.widgets.text.widget(field, default) 871 elif field.type == 'password': 872 inp = self.widgets.password.widget(field, default) 873 if self.record: 874 dspval = PasswordWidget.DEFAULT_PASSWORD_DISPLAY 875 else: 876 dspval = '' 877 elif field.type == 'blob': 878 continue 879 else: 880 inp = self.widgets.string.widget(field, default) 881 882 xfields.append((row_id,label,inp,comment)) 883 self.custom.dspval[fieldname] = dspval or nbsp 884 self.custom.inpval[fieldname] = inpval or '' 885 self.custom.widget[fieldname] = inp 886 887 # if a record is provided and found, as is linkto 888 # build a link 889 if record and linkto: 890 db = linkto.split('/')[-1] 891 for (rtable, rfield) in table._referenced_by: 892 if keyed: 893 rfld = table._db[rtable][rfield] 894 query = urllib.quote('%s.%s==%s' % (db,rfld,record[rfld.type[10:].split('.')[1]])) 895 else: 896 query = urllib.quote('%s.%s==%s' % (db,table._db[rtable][rfield],record.id)) 897 lname = olname = '%s.%s' % (rtable, rfield) 898 if ofields and not olname in ofields: 899 continue 900 if labels and lname in labels: 901 lname = labels[lname] 902 widget = A(lname, 903 _class='reference', 904 _href='%s/%s?query=%s' % (linkto, rtable, query)) 905 xfields.append((olname.replace('.', '__')+SQLFORM.ID_ROW_SUFFIX, 906 '',widget,col3.get(olname,''))) 907 self.custom.linkto[olname.replace('.', '__')] = widget 908 # </block> 909 910 # when deletable, add delete? checkbox 911 self.custom.deletable = '' 912 if record and deletable: 913 widget = INPUT(_type='checkbox', 914 _class='delete', 915 _id=self.FIELDKEY_DELETE_RECORD, 916 _name=self.FIELDNAME_REQUEST_DELETE, 917 ) 918 xfields.append((self.FIELDKEY_DELETE_RECORD+SQLFORM.ID_ROW_SUFFIX, 919 LABEL( 920 delete_label, 921 _for=self.FIELDKEY_DELETE_RECORD, 922 _id=self.FIELDKEY_DELETE_RECORD+SQLFORM.ID_LABEL_SUFFIX), 923 widget, 924 col3.get(self.FIELDKEY_DELETE_RECORD, ''))) 925 self.custom.deletable = widget 926 # when writable, add submit button 927 self.custom.submit = '' 928 if (not readonly) and ('submit' in buttons): 929 widget = INPUT(_type='submit', 930 _value=submit_button) 931 xfields.append(('submit_record'+SQLFORM.ID_ROW_SUFFIX, 932 '', widget,col3.get('submit_button', ''))) 933 self.custom.submit = widget 934 # if a record is provided and found 935 # make sure it's id is stored in the form 936 if record: 937 if not self['hidden']: 938 self['hidden'] = {} 939 if not keyed: 940 self['hidden']['id'] = record['id'] 941 942 (begin, end) = self._xml() 943 self.custom.begin = XML("<%s %s>" % (self.tag, begin)) 944 self.custom.end = XML("%s</%s>" % (end, self.tag)) 945 table = self.createform(xfields) 946 self.components = [table]
947
948 - def createform(self, xfields):
949 if self.formstyle == 'table3cols': 950 table = TABLE() 951 for id,a,b,c in xfields: 952 td_b = self.field_parent[id] = TD(b,_class='w2p_fw') 953 table.append(TR(TD(a,_class='w2p_fl'), 954 td_b, 955 TD(c,_class='w2p_fc'),_id=id)) 956 elif self.formstyle == 'table2cols': 957 table = TABLE() 958 for id,a,b,c in xfields: 959 td_b = self.field_parent[id] = TD(b,_class='w2p_fw',_colspan="2") 960 table.append(TR(TD(a,_class='w2p_fl'), 961 TD(c,_class='w2p_fc'),_id=id 962 +'1',_class='even')) 963 table.append(TR(td_b,_id=id+'2',_class='odd')) 964 elif self.formstyle == 'divs': 965 table = TAG['']() 966 for id,a,b,c in xfields: 967 div_b = self.field_parent[id] = DIV(b,_class='w2p_fw') 968 table.append(DIV(DIV(a,_class='w2p_fl'), 969 div_b, 970 DIV(c,_class='w2p_fc'),_id=id)) 971 elif self.formstyle == 'ul': 972 table = UL() 973 for id,a,b,c in xfields: 974 div_b = self.field_parent[id] = DIV(b,_class='w2p_fw') 975 table.append(LI(DIV(a,_class='w2p_fl'), 976 div_b, 977 DIV(c,_class='w2p_fc'),_id=id)) 978 elif type(self.formstyle) == type(lambda:None): 979 table = TABLE() 980 for id,a,b,c in xfields: 981 td_b = self.field_parent[id] = TD(b,_class='w2p_fw') 982 newrows = self.formstyle(id,a,td_b,c) 983 if type(newrows).__name__ != "tuple": 984 newrows = [newrows] 985 for newrow in newrows: 986 table.append(newrow) 987 else: 988 raise RuntimeError, 'formstyle not supported' 989 return table
990 991
992 - def accepts( 993 self, 994 request_vars, 995 session=None, 996 formname='%(tablename)s/%(record_id)s', 997 keepvalues=False, 998 onvalidation=None, 999 dbio=True, 1000 hideerror=False, 1001 detect_record_change=False, 1002 ):
1003 1004 """ 1005 similar FORM.accepts but also does insert, update or delete in DAL. 1006 but if detect_record_change == True than: 1007 form.record_changed = False (record is properly validated/submitted) 1008 form.record_changed = True (record cannot be submitted because changed) 1009 elseif detect_record_change == False than: 1010 form.record_changed = None 1011 """ 1012 1013 if request_vars.__class__.__name__ == 'Request': 1014 request_vars = request_vars.post_vars 1015 1016 keyed = hasattr(self.table, '_primarykey') 1017 1018 # implement logic to detect whether record exist but has been modified 1019 # server side 1020 self.record_changed = None 1021 if detect_record_change: 1022 if self.record: 1023 self.record_changed = False 1024 serialized = '|'.join(str(self.record[k]) for k in self.table.fields()) 1025 self.record_hash = md5_hash(serialized) 1026 1027 # logic to deal with record_id for keyed tables 1028 if self.record: 1029 if keyed: 1030 formname_id = '.'.join(str(self.record[k]) 1031 for k in self.table._primarykey 1032 if hasattr(self.record,k)) 1033 record_id = dict((k, request_vars[k]) for k in self.table._primarykey) 1034 else: 1035 (formname_id, record_id) = (self.record.id, 1036 request_vars.get('id', None)) 1037 keepvalues = True 1038 else: 1039 if keyed: 1040 formname_id = 'create' 1041 record_id = dict([(k, None) for k in self.table._primarykey]) 1042 else: 1043 (formname_id, record_id) = ('create', None) 1044 1045 if not keyed and isinstance(record_id, (list, tuple)): 1046 record_id = record_id[0] 1047 1048 if formname: 1049 formname = formname % dict(tablename = self.table._tablename, 1050 record_id = formname_id) 1051 1052 # ## THIS IS FOR UNIQUE RECORDS, read IS_NOT_IN_DB 1053 1054 for fieldname in self.fields: 1055 field = self.table[fieldname] 1056 requires = field.requires or [] 1057 if not isinstance(requires, (list, tuple)): 1058 requires = [requires] 1059 [item.set_self_id(self.record_id) for item in requires 1060 if hasattr(item, 'set_self_id') and self.record_id] 1061 1062 # ## END 1063 1064 fields = {} 1065 for key in self.vars: 1066 fields[key] = self.vars[key] 1067 1068 ret = FORM.accepts( 1069 self, 1070 request_vars, 1071 session, 1072 formname, 1073 keepvalues, 1074 onvalidation, 1075 hideerror=hideerror, 1076 ) 1077 1078 if not ret and self.record and self.errors: 1079 ### if there are errors in update mode 1080 # and some errors refers to an already uploaded file 1081 # delete error if 1082 # - user not trying to upload a new file 1083 # - there is existing file and user is not trying to delete it 1084 # this is because removing the file may not pass validation 1085 for key in self.errors.keys(): 1086 if key in self.table \ 1087 and self.table[key].type == 'upload' \ 1088 and request_vars.get(key, None) in (None, '') \ 1089 and self.record[key] \ 1090 and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars: 1091 del self.errors[key] 1092 if not self.errors: 1093 ret = True 1094 1095 requested_delete = \ 1096 request_vars.get(self.FIELDNAME_REQUEST_DELETE, False) 1097 1098 self.custom.end = TAG[''](self.hidden_fields(), self.custom.end) 1099 1100 auch = record_id and self.errors and requested_delete 1101 1102 # auch is true when user tries to delete a record 1103 # that does not pass validation, yet it should be deleted 1104 1105 if not ret and not auch: 1106 for fieldname in self.fields: 1107 field = self.table[fieldname] 1108 ### this is a workaround! widgets should always have default not None! 1109 if not field.widget and field.type.startswith('list:') and \ 1110 not OptionsWidget.has_options(field): 1111 field.widget = self.widgets.list.widget 1112 if hasattr(field, 'widget') and field.widget and fieldname in request_vars: 1113 if fieldname in self.vars: 1114 value = self.vars[fieldname] 1115 elif self.record: 1116 value = self.record[fieldname] 1117 else: 1118 value = self.table[fieldname].default 1119 if field.type.startswith('list:') and \ 1120 isinstance(value, str): 1121 value = [value] 1122 row_id = '%s_%s%s' % (self.table, fieldname, SQLFORM.ID_ROW_SUFFIX) 1123 widget = field.widget(field, value) 1124 self.field_parent[row_id].components = [ widget ] 1125 if not field.type.startswith('list:'): 1126 self.field_parent[row_id]._traverse(False, hideerror) 1127 self.custom.widget[ fieldname ] = widget 1128 self.accepted = ret 1129 return ret 1130 1131 if record_id and str(record_id) != str(self.record_id): 1132 raise SyntaxError, 'user is tampering with form\'s record_id: ' \ 1133 '%s != %s' % (record_id, self.record_id) 1134 1135 if record_id and dbio and not keyed: 1136 self.vars.id = self.record.id 1137 1138 if requested_delete and self.custom.deletable: 1139 if dbio: 1140 if keyed: 1141 qry = reduce(lambda x, y: x & y, 1142 [self.table[k] == record_id[k] for k in self.table._primarykey]) 1143 else: 1144 qry = self.table._id == self.record.id 1145 self.table._db(qry).delete() 1146 self.errors.clear() 1147 for component in self.elements('input, select, textarea'): 1148 component['_disabled'] = True 1149 self.accepted = True 1150 return True 1151 1152 for fieldname in self.fields: 1153 if not fieldname in self.table.fields: 1154 continue 1155 1156 if not self.ignore_rw and not self.table[fieldname].writable: 1157 ### this happens because FORM has no knowledge of writable 1158 ### and thinks that a missing boolean field is a None 1159 if self.table[fieldname].type == 'boolean' and \ 1160 self.vars.get(fieldname, True) is None: 1161 del self.vars[fieldname] 1162 continue 1163 1164 field = self.table[fieldname] 1165 if field.type == 'id': 1166 continue 1167 if field.type == 'boolean': 1168 if self.vars.get(fieldname, False): 1169 self.vars[fieldname] = fields[fieldname] = True 1170 else: 1171 self.vars[fieldname] = fields[fieldname] = False 1172 elif field.type == 'password' and self.record\ 1173 and request_vars.get(fieldname, None) == \ 1174 PasswordWidget.DEFAULT_PASSWORD_DISPLAY: 1175 continue # do not update if password was not changed 1176 elif field.type == 'upload': 1177 f = self.vars[fieldname] 1178 fd = '%s__delete' % fieldname 1179 if f == '' or f is None: 1180 if self.vars.get(fd, False) or not self.record: 1181 fields[fieldname] = '' 1182 else: 1183 fields[fieldname] = self.record[fieldname] 1184 self.vars[fieldname] = fields[fieldname] 1185 continue 1186 elif hasattr(f, 'file'): 1187 (source_file, original_filename) = (f.file, f.filename) 1188 elif isinstance(f, (str, unicode)): 1189 ### do not know why this happens, it should not 1190 (source_file, original_filename) = \ 1191 (cStringIO.StringIO(f), 'file.txt') 1192 newfilename = field.store(source_file, original_filename) 1193 # this line is for backward compatibility only 1194 self.vars['%s_newfilename' % fieldname] = newfilename 1195 fields[fieldname] = newfilename 1196 if isinstance(field.uploadfield, str): 1197 fields[field.uploadfield] = source_file.read() 1198 # proposed by Hamdy (accept?) do we need fields at this point? 1199 self.vars[fieldname] = fields[fieldname] 1200 continue 1201 elif fieldname in self.vars: 1202 fields[fieldname] = self.vars[fieldname] 1203 elif field.default is None and field.type != 'blob': 1204 self.errors[fieldname] = 'no data' 1205 self.accepted = False 1206 return False 1207 value = fields.get(fieldname,None) 1208 if field.type == 'list:string': 1209 if not isinstance(value, (tuple, list)): 1210 fields[fieldname] = value and [value] or [] 1211 elif isinstance(field.type,str) and field.type.startswith('list:'): 1212 if not isinstance(value, list): 1213 fields[fieldname] = [safe_int(x) for x in (value and [value] or [])] 1214 elif field.type == 'integer': 1215 if not value is None: 1216 fields[fieldname] = safe_int(value) 1217 elif field.type.startswith('reference'): 1218 if not value is None and isinstance(self.table, Table) and not keyed: 1219 fields[fieldname] = safe_int(value) 1220 elif field.type == 'double': 1221 if not value is None: 1222 fields[fieldname] = safe_float(value) 1223 1224 for fieldname in self.vars: 1225 if fieldname != 'id' and fieldname in self.table.fields\ 1226 and not fieldname in fields and not fieldname\ 1227 in request_vars: 1228 fields[fieldname] = self.vars[fieldname] 1229 1230 if dbio: 1231 if 'delete_this_record' in fields: 1232 # this should never happen but seems to happen to some 1233 del fields['delete_this_record'] 1234 for field in self.table: 1235 if not field.name in fields and field.writable==False \ 1236 and field.update is None: 1237 if record_id: 1238 fields[field.name] = self.record[field.name] 1239 elif not self.table[field.name].default is None: 1240 fields[field.name] = self.table[field.name].default 1241 if keyed: 1242 if reduce(lambda x, y: x and y, record_id.values()): # if record_id 1243 if fields: 1244 qry = reduce(lambda x, y: x & y, 1245 [self.table[k] == self.record[k] for k in self.table._primarykey]) 1246 self.table._db(qry).update(**fields) 1247 else: 1248 pk = self.table.insert(**fields) 1249 if pk: 1250 self.vars.update(pk) 1251 else: 1252 ret = False 1253 else: 1254 if record_id: 1255 self.vars.id = self.record.id 1256 if fields: 1257 self.table._db(self.table._id == self.record.id).update(**fields) 1258 else: 1259 self.vars.id = self.table.insert(**fields) 1260 self.accepted = ret 1261 return ret
1262 1263 @staticmethod
1264 - def factory(*fields, **attributes):
1265 """ 1266 generates a SQLFORM for the given fields. 1267 1268 Internally will build a non-database based data model 1269 to hold the fields. 1270 """ 1271 # Define a table name, this way it can be logical to our CSS. 1272 # And if you switch from using SQLFORM to SQLFORM.factory 1273 # your same css definitions will still apply. 1274 1275 table_name = attributes.get('table_name', 'no_table') 1276 1277 # So it won't interfear with SQLDB.define_table 1278 if 'table_name' in attributes: 1279 del attributes['table_name'] 1280 1281 return SQLFORM(DAL(None).define_table(table_name, *fields), 1282 **attributes)
1283 1284 @staticmethod
1285 - def grid(query, 1286 fields=None, 1287 field_id=None, 1288 left=None, 1289 headers={}, 1290 columns=None, 1291 orderby=None, 1292 searchable=True, 1293 sortable=True, 1294 paginate=20, 1295 deletable=True, 1296 editable=True, 1297 details=True, 1298 selectable=None, 1299 create=True, 1300 csv=True, 1301 links=None, 1302 upload = '<default>', 1303 args=[], 1304 user_signature = True, 1305 maxtextlengths={}, 1306 maxtextlength=20, 1307 onvalidation=None, 1308 oncreate=None, 1309 onupdate=None, 1310 ondelete=None, 1311 sorter_icons=('[^]','[v]'), 1312 ui = 'web2py', 1313 showbuttontext=True, 1314 _class="web2py_grid", 1315 formname='web2py_grid', 1316 ):
1317 1318 # jQuery UI ThemeRoller classes (empty if ui is disabled) 1319 if ui == 'jquery-ui': 1320 ui = dict(widget='ui-widget', 1321 header='ui-widget-header', 1322 content='ui-widget-content', 1323 default='ui-state-default', 1324 cornerall='ui-corner-all', 1325 cornertop='ui-corner-top', 1326 cornerbottom='ui-corner-bottom', 1327 button='ui-button-text-icon-primary', 1328 buttontext='ui-button-text', 1329 buttonadd='ui-icon ui-icon-plusthick', 1330 buttonback='ui-icon ui-icon-arrowreturnthick-1-w', 1331 buttonexport='ui-icon ui-icon-transferthick-e-w', 1332 buttondelete='ui-icon ui-icon-trash', 1333 buttonedit='ui-icon ui-icon-pencil', 1334 buttontable='ui-icon ui-icon-triangle-1-e', 1335 buttonview='ui-icon ui-icon-zoomin', 1336 ) 1337 elif ui == 'web2py': 1338 ui = dict(widget='', 1339 header='', 1340 content='', 1341 default='', 1342 cornerall='', 1343 cornertop='', 1344 cornerbottom='', 1345 button='button', 1346 buttontext='buttontext button', 1347 buttonadd='icon plus', 1348 buttonback='icon leftarrow', 1349 buttonexport='icon downarrow', 1350 buttondelete='icon trash', 1351 buttonedit='icon pen', 1352 buttontable='icon rightarrow', 1353 buttonview='icon magnifier', 1354 ) 1355 elif not isinstance(ui,dict): 1356 raise RuntimeError,'SQLFORM.grid ui argument must be a dictionary' 1357 1358 from gluon import current, redirect 1359 db = query._db 1360 T = current.T 1361 request = current.request 1362 session = current.session 1363 response = current.response 1364 wenabled = (not user_signature or (session.auth and session.auth.user)) 1365 #create = wenabled and create 1366 #editable = wenabled and editable 1367 deletable = wenabled and deletable 1368 def url(**b): 1369 b['args'] = args+b.get('args',[]) 1370 b['user_signature'] = user_signature 1371 return URL(**b)
1372 1373 def gridbutton(buttonclass='buttonadd',buttontext='Add',buttonurl=url(args=[]),callback=None,delete=None): 1374 if showbuttontext: 1375 if callback: 1376 return A(SPAN(_class=ui.get(buttonclass,'')), 1377 SPAN(T(buttontext),_title=buttontext, 1378 _class=ui.get('buttontext','')), 1379 callback=callback,delete=delete, 1380 _class=ui.get('button','')) 1381 else: 1382 return A(SPAN(_class=ui.get(buttonclass,'')), 1383 SPAN(T(buttontext),_title=buttontext, 1384 _class=ui.get('buttontext','')), 1385 _href=buttonurl,_class=ui.get('button','')) 1386 else: 1387 if callback: 1388 return A(SPAN(_class=ui.get(buttonclass,'')), 1389 callback=callback,delete=delete, 1390 _title=buttontext,_class=ui.get('buttontext','')) 1391 else: 1392 return A(SPAN(_class=ui.get(buttonclass,'')), 1393 _href=buttonurl,_title=buttontext, 1394 _class=ui.get('buttontext',''))
1395 1396 dbset = db(query) 1397 tables = [db[tablename] for tablename in db._adapter.tables( 1398 dbset.query)] 1399 if not fields: 1400 fields = reduce(lambda a,b:a+b, 1401 [[field for field in table] for table in tables]) 1402 if not field_id: 1403 field_id = tables[0]._id 1404 table = field_id.table 1405 tablename = table._tablename 1406 referrer = session.get('_web2py_grid_referrer_'+formname, url()) 1407 def check_authorization(): 1408 if user_signature: 1409 if not URL.verify(request,user_signature=user_signature): 1410 session.flash = T('not authorized') 1411 redirect(referrer) 1412 if upload=='<default>': 1413 upload = lambda filename: url(args=['download',filename]) 1414 if len(request.args)>1 and request.args[-2]=='download': 1415 check_authorization() 1416 stream = response.download(request,db) 1417 raise HTTP(200,stream,**response.headers) 1418 1419 def buttons(edit=False,view=False,record=None): 1420 buttons = DIV(gridbutton('buttonback', 'Back', referrer), 1421 _class='form_header row_buttons %(header)s %(cornertop)s' % ui) 1422 if edit: 1423 args = ['edit',table._tablename,request.args[-1]] 1424 buttons.append(gridbutton('buttonedit', 'Edit', 1425 url(args=args))) 1426 if view: 1427 args = ['view',table._tablename,request.args[-1]] 1428 buttons.append(gridbutton('buttonview', 'View', 1429 url(args=args))) 1430 if record and links: 1431 for link in links: 1432 buttons.append(link(record)) 1433 return buttons 1434 1435 formfooter = DIV( 1436 _class='form_footer row_buttons %(header)s %(cornerbottom)s' % ui) 1437 1438 create_form = edit_form = None 1439 1440 if create and len(request.args)>1 and request.args[-2]=='new': 1441 check_authorization() 1442 table = db[request.args[-1]] 1443 create_form = SQLFORM( 1444 table, 1445 _class='web2py_form' 1446 ).process(next=referrer, 1447 onvalidation=onvalidation, 1448 onsuccess=oncreate, 1449 formname=formname) 1450 res = DIV(buttons(),create_form,formfooter,_class=_class) 1451 res.create_form = create_form 1452 res.edit_form = None 1453 res.update_form = None 1454 return res 1455 elif details and len(request.args)>2 and request.args[-3]=='view': 1456 check_authorization() 1457 table = db[request.args[-2]] 1458 record = table(request.args[-1]) or redirect(URL('error')) 1459 form = SQLFORM(table,record,upload=upload, 1460 readonly=True,_class='web2py_form') 1461 res = DIV(buttons(edit=editable,record=record),form, 1462 formfooter,_class=_class) 1463 res.create_form = None 1464 res.edit_form = None 1465 res.update_form = None 1466 return res 1467 elif editable and len(request.args)>2 and request.args[-3]=='edit': 1468 check_authorization() 1469 table = db[request.args[-2]] 1470 record = table(request.args[-1]) or redirect(URL('error')) 1471 edit_form = SQLFORM(table,record,upload=upload, 1472 deletable=deletable, 1473 _class='web2py_form') 1474 edit_form.process(formname=formname, 1475 onvalidation=onvalidation, 1476 onsuccess=onupdate, 1477 next=referrer) 1478 res = DIV(buttons(view=details,record=record), 1479 edit_form,formfooter,_class=_class) 1480 res.create_form = None 1481 res.edit_form = edit_form 1482 res.update_form = None 1483 return res 1484 elif deletable and len(request.args)>2 and request.args[-3]=='delete': 1485 check_authorization() 1486 table = db[request.args[-2]] 1487 ret = db(table.id==request.args[-1]).delete() 1488 if ondelete: 1489 return ondelete(table,request.args[-2],ret) 1490 return ret 1491 elif csv and len(request.args)>0 and request.args[-1]=='csv': 1492 check_authorization() 1493 response.headers['Content-Type'] = 'text/csv' 1494 response.headers['Content-Disposition'] = \ 1495 'attachment;filename=rows.csv;' 1496 raise HTTP(200,str(dbset.select()), 1497 **{'Content-Type':'text/csv', 1498 'Content-Disposition':'attachment;filename=rows.csv;'}) 1499 elif request.vars.records and not isinstance( 1500 request.vars.records,list): 1501 request.vars.records=[request.vars.records] 1502 elif not request.vars.records: 1503 request.vars.records=[] 1504 def OR(a,b): return a|b 1505 def AND(a,b): return a&b 1506 1507 session['_web2py_grid_referrer_'+formname] = \ 1508 URL(args=request.args,vars=request.vars, 1509 user_signature=user_signature) 1510 console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui) 1511 error = None 1512 search_form = None 1513 if searchable: 1514 form = FORM(INPUT(_name='keywords',_value=request.vars.keywords, 1515 _id='web2py_keywords'), 1516 INPUT(_type='submit',_value=T('Search')), 1517 INPUT(_type='submit',_value=T('Clear'), 1518 _onclick="jQuery('#web2py_keywords').val('');"), 1519 _method="GET",_action=url()) 1520 search_form = form 1521 console.append(form) 1522 key = request.vars.get('keywords','').strip() 1523 if searchable==True: 1524 subquery = None 1525 if key and not ' ' in key: 1526 SEARCHABLE_TYPES = ('string','text','list:string') 1527 parts = [field.contains(key) for field in fields \ 1528 if field.type in SEARCHABLE_TYPES] 1529 else: 1530 parts = None 1531 if parts: 1532 subquery = reduce(OR,parts) 1533 else: 1534 try: 1535 subquery = smart_query(fields,key) 1536 except RuntimeError: 1537 subquery = None 1538 error = T('Invalid query') 1539 else: 1540 subquery = searchable(key,fields) 1541 if subquery: 1542 dbset = dbset(subquery) 1543 try: 1544 if left: 1545 nrows = dbset.select('count(*)',left=left).first()['count(*)'] 1546 else: 1547 nrows = dbset.count() 1548 except: 1549 nrows = 0 1550 error = T('Unsupported query') 1551 1552 search_actions = DIV(_class='web2py_search_actions') 1553 if create: 1554 search_actions.append(gridbutton( 1555 buttonclass='buttonadd', 1556 buttontext='Add', 1557 buttonurl=url(args=['new',tablename]))) 1558 if csv: 1559 search_actions.append(gridbutton( 1560 buttonclass='buttonexport', 1561 buttontext='Export', 1562 buttonurl=url(args=['csv']))) 1563 1564 console.append(search_actions) 1565 1566 message = error or T('%(nrows)s records found' % dict(nrows=nrows)) 1567 1568 console.append(DIV(message,_class='web2py_counter')) 1569 1570 order = request.vars.order or '' 1571 if sortable: 1572 if order and not order=='None': 1573 if order[:1]=='~': 1574 sign, rorder = '~', order[1:] 1575 else: 1576 sign, rorder = '', order 1577 tablename,fieldname = rorder.split('.',1) 1578 if sign=='~': 1579 orderby=~db[tablename][fieldname] 1580 else: 1581 orderby=db[tablename][fieldname] 1582 1583 head = TR(_class=ui.get('header','')) 1584 if selectable: 1585 head.append(TH(_class=ui.get('default',''))) 1586 for field in fields: 1587 if columns and not str(field) in columns: continue 1588 if not field.readable: continue 1589 key = str(field) 1590 header = headers.get(str(field), 1591 hasattr(field,'label') and field.label or key) 1592 if sortable: 1593 if key == order: 1594 key, marker = '~'+order, sorter_icons[0] 1595 elif key == order[1:]: 1596 marker = sorter_icons[1] 1597 else: 1598 marker = '' 1599 header = A(header,marker,_href=url(vars=dict( 1600 keywords=request.vars.keywords or '', 1601 order=key))) 1602 head.append(TH(header, _class=ui.get('default',''))) 1603 1604 for link in links or []: 1605 if isinstance(link,dict): 1606 head.append(TH(link['header'], _class=ui.get('default',''))) 1607 1608 head.append(TH(_class=ui.get('default',''))) 1609 1610 paginator = UL() 1611 if paginate and paginate<nrows: 1612 npages,reminder = divmod(nrows,paginate) 1613 if reminder: npages+=1 1614 try: page = int(request.vars.page or 1)-1 1615 except ValueError: page = 0 1616 limitby = (paginate*page,paginate*(page+1)) 1617 def self_link(name,p): 1618 d = dict(page=p+1) 1619 if order: d['order']=order 1620 if request.vars.keywords: d['keywords']=request.vars.keywords 1621 return A(name,_href=url(vars=d)) 1622 if page>0: 1623 paginator.append(LI(self_link('<<',0))) 1624 if page>1: 1625 paginator.append(LI(self_link('<',page-1))) 1626 pages = range(max(0,page-5),min(page+5,npages-1)) 1627 for p in pages: 1628 if p == page: 1629 paginator.append(LI(A(p+1,_onclick='return false'), 1630 _class='current')) 1631 else: 1632 paginator.append(LI(self_link(p+1,p))) 1633 if page<npages-2: 1634 paginator.append(LI(self_link('>',page+1))) 1635 if page<npages-1: 1636 paginator.append(LI(self_link('>>',npages-1))) 1637 else: 1638 limitby = None 1639 1640 rows = dbset.select(left=left,orderby=orderby,limitby=limitby,*fields) 1641 if not searchable and not rows: return DIV(T('No records found')) 1642 if rows: 1643 htmltable = TABLE(THEAD(head)) 1644 tbody = TBODY() 1645 numrec=0 1646 for row in rows: 1647 if numrec % 2 == 0: 1648 classtr = 'even' 1649 else: 1650 classtr = 'odd' 1651 numrec+=1 1652 id = row[field_id] 1653 if len(tables)>1 or row.get('_extra',None): 1654 rrow = row[field._tablename] 1655 else: 1656 rrow = row 1657 tr = TR(_class=classtr) 1658 if selectable: 1659 tr.append(INPUT(_type="checkbox",_name="records",_value=id, 1660 value=request.vars.records)) 1661 for field in fields: 1662 if columns and not str(field) in columns: continue 1663 if not field.readable: continue 1664 if field.type=='blob': continue 1665 value = row[field] 1666 if field.represent: 1667 try: 1668 value=field.represent(value,rrow) 1669 except KeyError: 1670 pass 1671 elif field.type=='boolean': 1672 value = INPUT(_type="checkbox",_checked = value, 1673 _disabled=True) 1674 elif field.type=='upload': 1675 if value: 1676 if callable(upload): 1677 value = A('File', _href=upload(value)) 1678 elif upload: 1679 value = A('File', 1680 _href='%s/%s' % (upload, value)) 1681 else: 1682 value = '' 1683 elif isinstance(value,str) and len(value)>maxtextlength: 1684 value=value[:maxtextlengths.get(str(field),maxtextlength)]+'...' 1685 else: 1686 value=field.formatter(value) 1687 tr.append(TD(value)) 1688 row_buttons = TD(_class='row_buttons') 1689 for link in links or []: 1690 if isinstance(link, dict): 1691 tr.append(TD(link['body'](row))) 1692 else: 1693 row_buttons.append(link(row)) 1694 if details and (not callable(details) or details(row)): 1695 row_buttons.append(gridbutton( 1696 'buttonview', 'View', 1697 url(args=['view',tablename,id]))) 1698 if editable and (not callable(editable) or editable(row)): 1699 row_buttons.append(gridbutton( 1700 'buttonedit', 'Edit', 1701 url(args=['edit',tablename,id]))) 1702 if deletable and (not callable(deletable) or deletable(row)): 1703 row_buttons.append(gridbutton( 1704 'buttondelete', 'Delete', 1705 callback=url(args=['delete',tablename,id]), 1706 delete='tr')) 1707 tr.append(row_buttons) 1708 tbody.append(tr) 1709 htmltable.append(tbody) 1710 if selectable: 1711 htmltable = FORM(htmltable,INPUT(_type="submit")) 1712 if htmltable.process(formname=formname).accepted: 1713 records = [int(r) for r in htmltable.vars.records or []] 1714 selectable(records) 1715 redirect(referrer) 1716 else: 1717 htmltable = DIV(T('No records found')) 1718 res = DIV(console, 1719 DIV(htmltable,_class="web2py_table"), 1720 DIV(paginator,_class=\ 1721 "web2py_paginator %(header)s %(cornerbottom)s" % ui), 1722 _class='%s %s' % (_class, ui.get('widget',''))) 1723 res.create_form = create_form 1724 res.edit_form = edit_form 1725 res.search_form = search_form 1726 return res 1727 1728 @staticmethod
1729 - def smartgrid(table, constraints=None, links=None, 1730 linked_tables=None, user_signature=True, 1731 **kwargs):
1732 """ 1733 @auth.requires_login() 1734 def index(): 1735 db.define_table('person',Field('name'),format='%(name)s') 1736 db.define_table('dog', 1737 Field('name'),Field('owner',db.person),format='%(name)s') 1738 db.define_table('comment',Field('body'),Field('dog',db.dog)) 1739 if db(db.person).isempty(): 1740 from gluon.contrib.populate import populate 1741 populate(db.person,300) 1742 populate(db.dog,300) 1743 populate(db.comment,1000) 1744 db.commit() 1745 form=SQLFORM.smartgrid(db[request.args(0) or 'person']) #*** 1746 return dict(form=form) 1747 1748 *** builds a complete interface to navigate all tables links 1749 to the request.args(0) 1750 table: pagination, search, view, edit, delete, 1751 children, parent, etc. 1752 1753 constraints is a dict {'table',query} that limits which 1754 records can be accessible 1755 links is a list of lambda row: A(....) that will add buttons 1756 linked_tables is a optional list of tablenames of tables to be linked 1757 1758 """ 1759 from gluon import current, A, URL, DIV, H3, redirect 1760 request, T = current.request, current.T 1761 db = table._db 1762 if links is None: links = [] 1763 if constraints is None: constraints = {} 1764 breadcrumbs = [] 1765 if request.args(0) != table._tablename: 1766 request.args=[table._tablename] 1767 try: 1768 args = 1 1769 previous_tablename,previous_fieldname,previous_id = \ 1770 table._tablename,None,None 1771 while len(request.args)>args: 1772 key = request.args(args) 1773 if '.' in key: 1774 id = request.args(args+1) 1775 tablename,fieldname = key.split('.',1) 1776 table = db[tablename] 1777 field = table[fieldname] 1778 field.default = id 1779 referee = field.type[10:] 1780 if referee!=previous_tablename: 1781 raise HTTP(400) 1782 cond = constraints.get(referee,None) 1783 if cond: 1784 record = db(db[referee].id==id)(cond).select().first() 1785 else: 1786 record = db[referee](id) 1787 if previous_id: 1788 if record[previous_fieldname] != int(previous_id): 1789 raise HTTP(400) 1790 previous_tablename,previous_fieldname,previous_id = \ 1791 tablename,fieldname,id 1792 try: 1793 name = db[referee]._format % record 1794 except TypeError: 1795 name = id 1796 breadcrumbs += [A(T(referee), 1797 _href=URL(args=request.args[:args])),' ', 1798 A(name, 1799 _href=URL(args=request.args[:args]+[ 1800 'view',referee,id],user_signature=True)), 1801 ' > '] 1802 args+=2 1803 else: 1804 break 1805 if args>1: 1806 query = (field == id) 1807 if linked_tables is None or referee in linked_tables: 1808 field.represent = lambda id,r=None,referee=referee,rep=field.represent: A(rep(id),_href=URL(args=request.args[:args]+['view',referee,id], user_signature=user_signature)) 1809 except (KeyError,ValueError,TypeError): 1810 redirect(URL(args=table._tablename)) 1811 if args==1: 1812 query = table.id>0 1813 if table._tablename in constraints: 1814 query = query&constraints[table._tablename] 1815 for tablename,fieldname in table._referenced_by: 1816 if linked_tables is None or tablename in linked_tables: 1817 args0 = tablename+'.'+fieldname 1818 links.append(lambda row,t=T(tablename),args=args,args0=args0:\ 1819 A(SPAN(t),_href=URL(args=request.args[:args]+[args0,row.id]))) 1820 grid=SQLFORM.grid(query,args=request.args[:args],links=links, 1821 user_signature=user_signature,**kwargs) 1822 if isinstance(grid,DIV): 1823 breadcrumbs.append(A(T(table._tablename), 1824 _href=URL(args=request.args[:args]))) 1825 grid.insert(0,DIV(H3(*breadcrumbs),_class='web2py_breadcrumbs')) 1826 return grid
1827 1828
1829 -class SQLTABLE(TABLE):
1830 1831 """ 1832 given a Rows object, as returned by a db().select(), generates 1833 an html table with the rows. 1834 1835 optional arguments: 1836 1837 :param linkto: URL (or lambda to generate a URL) to edit individual records 1838 :param upload: URL to download uploaded files 1839 :param orderby: Add an orderby link to column headers. 1840 :param headers: dictionary of headers to headers redefinions 1841 headers can also be a string to gerenare the headers from data 1842 for now only headers="fieldname:capitalize", 1843 headers="labels" and headers=None are supported 1844 :param truncate: length at which to truncate text in table cells. 1845 Defaults to 16 characters. 1846 :param columns: a list or dict contaning the names of the columns to be shown 1847 Defaults to all 1848 1849 Optional names attributes for passed to the <table> tag 1850 1851 The keys of headers and columns must be of the form "tablename.fieldname" 1852 1853 Simple linkto example:: 1854 1855 rows = db.select(db.sometable.ALL) 1856 table = SQLTABLE(rows, linkto='someurl') 1857 1858 This will link rows[id] to .../sometable/value_of_id 1859 1860 1861 More advanced linkto example:: 1862 1863 def mylink(field, type, ref): 1864 return URL(args=[field]) 1865 1866 rows = db.select(db.sometable.ALL) 1867 table = SQLTABLE(rows, linkto=mylink) 1868 1869 This will link rows[id] to 1870 current_app/current_controlle/current_function/value_of_id 1871 1872 New Implements: 24 June 2011: 1873 ----------------------------- 1874 1875 :param selectid: The id you want to select 1876 :param renderstyle: Boolean render the style with the table 1877 1878 :param extracolums = [{'label':A('Extra',_href='#'), 1879 'class': '', #class name of the header 1880 'width':'', #width in pixels or % 1881 'content':lambda row, rc: A('Edit',_href='edit/%s'%row.id), 1882 'selected': False #agregate class selected to this column 1883 }] 1884 1885 1886 :param headers = {'table.id':{'label':'Id', 1887 'class':'', #class name of the header 1888 'width':'', #width in pixels or % 1889 'truncate': 16, #truncate the content to... 1890 'selected': False #agregate class selected to this column 1891 }, 1892 'table.myfield':{'label':'My field', 1893 'class':'', #class name of the header 1894 'width':'', #width in pixels or % 1895 'truncate': 16, #truncate the content to... 1896 'selected': False #agregate class selected to this column 1897 }, 1898 } 1899 1900 table = SQLTABLE(rows, headers=headers, extracolums=extracolums) 1901 1902 1903 """ 1904
1905 - def __init__( 1906 self, 1907 sqlrows, 1908 linkto=None, 1909 upload=None, 1910 orderby=None, 1911 headers={}, 1912 truncate=16, 1913 columns=None, 1914 th_link='', 1915 extracolumns=None, 1916 selectid=None, 1917 renderstyle=False, 1918 **attributes 1919 ):
1920 1921 TABLE.__init__(self, **attributes) 1922 1923 self.components = [] 1924 self.attributes = attributes 1925 self.sqlrows = sqlrows 1926 (components, row) = (self.components, []) 1927 if not sqlrows: 1928 return 1929 if not columns: 1930 columns = sqlrows.colnames 1931 if headers=='fieldname:capitalize': 1932 headers = {} 1933 for c in columns: 1934 headers[c] = ' '.join([w.capitalize() for w in c.split('.')[-1].split('_')]) 1935 elif headers=='labels': 1936 headers = {} 1937 for c in columns: 1938 (t,f) = c.split('.') 1939 field = sqlrows.db[t][f] 1940 headers[c] = field.label 1941 if not headers is None: 1942 for c in columns:#new implement dict 1943 if isinstance(headers.get(c, c), dict): 1944 coldict = headers.get(c, c) 1945 attrcol = dict() 1946 if coldict['width']!="": 1947 attrcol.update(_width=coldict['width']) 1948 if coldict['class']!="": 1949 attrcol.update(_class=coldict['class']) 1950 row.append(TH(coldict['label'],**attrcol)) 1951 elif orderby: 1952 row.append(TH(A(headers.get(c, c), 1953 _href=th_link+'?orderby=' + c))) 1954 else: 1955 row.append(TH(headers.get(c, c))) 1956 1957 if extracolumns:#new implement dict 1958 for c in extracolumns: 1959 attrcol = dict() 1960 if c['width']!="": 1961 attrcol.update(_width=c['width']) 1962 if c['class']!="": 1963 attrcol.update(_class=c['class']) 1964 row.append(TH(c['label'],**attrcol)) 1965 1966 components.append(THEAD(TR(*row))) 1967 1968 1969 tbody = [] 1970 for (rc, record) in enumerate(sqlrows): 1971 row = [] 1972 if rc % 2 == 0: 1973 _class = 'even' 1974 else: 1975 _class = 'odd' 1976 1977 if not selectid is None: #new implement 1978 if record.id==selectid: 1979 _class += ' rowselected' 1980 1981 for colname in columns: 1982 if not table_field.match(colname): 1983 if "_extra" in record and colname in record._extra: 1984 r = record._extra[colname] 1985 row.append(TD(r)) 1986 continue 1987 else: 1988 raise KeyError("Column %s not found (SQLTABLE)" % colname) 1989 (tablename, fieldname) = colname.split('.') 1990 try: 1991 field = sqlrows.db[tablename][fieldname] 1992 except KeyError: 1993 field = None 1994 if tablename in record \ 1995 and isinstance(record,Row) \ 1996 and isinstance(record[tablename],Row): 1997 r = record[tablename][fieldname] 1998 elif fieldname in record: 1999 r = record[fieldname] 2000 else: 2001 raise SyntaxError, 'something wrong in Rows object' 2002 r_old = r 2003 if not field: 2004 pass 2005 elif linkto and field.type == 'id': 2006 try: 2007 href = linkto(r, 'table', tablename) 2008 except TypeError: 2009 href = '%s/%s/%s' % (linkto, tablename, r_old) 2010 r = A(r, _href=href) 2011 elif field.type.startswith('reference'): 2012 if linkto: 2013 ref = field.type[10:] 2014 try: 2015 href = linkto(r, 'reference', ref) 2016 except TypeError: 2017 href = '%s/%s/%s' % (linkto, ref, r_old) 2018 if ref.find('.') >= 0: 2019 tref,fref = ref.split('.') 2020 if hasattr(sqlrows.db[tref],'_primarykey'): 2021 href = '%s/%s?%s' % (linkto, tref, urllib.urlencode({fref:r})) 2022 r = A(represent(field,r,record), _href=str(href)) 2023 elif field.represent: 2024 r = represent(field,r,record) 2025 elif linkto and hasattr(field._table,'_primarykey') and fieldname in field._table._primarykey: 2026 # have to test this with multi-key tables 2027 key = urllib.urlencode(dict( [ \ 2028 ((tablename in record \ 2029 and isinstance(record, Row) \ 2030 and isinstance(record[tablename], Row)) and 2031 (k, record[tablename][k])) or (k, record[k]) \ 2032 for k in field._table._primarykey ] )) 2033 r = A(r, _href='%s/%s?%s' % (linkto, tablename, key)) 2034 elif field.type.startswith('list:'): 2035 r = represent(field,r or [],record) 2036 elif field.represent: 2037 r = represent(field,r,record) 2038 elif field.type == 'blob' and r: 2039 r = 'DATA' 2040 elif field.type == 'upload': 2041 if upload and r: 2042 r = A('file', _href='%s/%s' % (upload, r)) 2043 elif r: 2044 r = 'file' 2045 else: 2046 r = '' 2047 elif field.type in ['string','text']: 2048 r = str(field.formatter(r)) 2049 ur = unicode(r, 'utf8') 2050 if headers!={}: #new implement dict 2051 if isinstance(headers[colname],dict): 2052 if isinstance(headers[colname]['truncate'], int) \ 2053 and len(ur)>headers[colname]['truncate']: 2054 r = ur[:headers[colname]['truncate'] - 3] 2055 r = r.encode('utf8') + '...' 2056 elif not truncate is None and len(ur) > truncate: 2057 r = ur[:truncate - 3].encode('utf8') + '...' 2058 2059 attrcol = dict()#new implement dict 2060 if headers!={}: 2061 if isinstance(headers[colname],dict): 2062 colclass=headers[colname]['class'] 2063 if headers[colname]['selected']: 2064 colclass= str(headers[colname]['class'] + " colselected").strip() 2065 if colclass!="": 2066 attrcol.update(_class=colclass) 2067 2068 row.append(TD(r,**attrcol)) 2069 2070 if extracolumns:#new implement dict 2071 for c in extracolumns: 2072 attrcol = dict() 2073 colclass=c['class'] 2074 if c['selected']: 2075 colclass= str(c['class'] + " colselected").strip() 2076 if colclass!="": 2077 attrcol.update(_class=colclass) 2078 contentfunc = c['content'] 2079 row.append(TD(contentfunc(record, rc),**attrcol)) 2080 2081 tbody.append(TR(_class=_class, *row)) 2082 2083 if renderstyle: 2084 components.append(STYLE(self.style())) 2085 2086 components.append(TBODY(*tbody))
2087 2088
2089 - def style(self):
2090 2091 css = ''' 2092 table tbody tr.odd { 2093 background-color: #DFD; 2094 } 2095 table tbody tr.even { 2096 background-color: #EFE; 2097 } 2098 table tbody tr.rowselected { 2099 background-color: #FDD; 2100 } 2101 table tbody tr td.colselected { 2102 background-color: #FDD; 2103 } 2104 table tbody tr:hover { 2105 background: #DDF; 2106 } 2107 ''' 2108 2109 return css
2110 2111 form_factory = SQLFORM.factory # for backward compatibility, deprecated 2112