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, 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 as Url 
  22  from dal import DAL, Table, Row, CALLABLETYPES 
  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   
  32  table_field = re.compile('[\w_]+\.[\w_]+') 
  33  widget_class = re.compile('^\w*') 
  34   
35 -def safe_int(x):
36 try: 37 return int(x) 38 except ValueError: 39 return 0
40
41 -def safe_float(x):
42 try: 43 return float(x) 44 except ValueError: 45 return 0
46
47 -class FormWidget(object):
48 """ 49 helper for SQLFORM to generate form input fields (widget), 50 related to the fieldtype 51 """ 52 53 @staticmethod
54 - def _attributes(field, widget_attributes, **attributes):
55 """ 56 helper to build a common set of attributes 57 58 :param field: the field involved, some attributes are derived from this 59 :param widget_attributes: widget related attributes 60 :param attributes: any other supplied attributes 61 """ 62 attr = dict( 63 _id = '%s_%s' % (field._tablename, field.name), 64 _class = widget_class.match(str(field.type)).group(), 65 _name = field.name, 66 requires = field.requires, 67 ) 68 attr.update(widget_attributes) 69 attr.update(attributes) 70 return attr
71 72 @staticmethod
73 - def widget(field, value, **attributes):
74 """ 75 generates the widget for the field. 76 77 When serialized, will provide an INPUT tag: 78 79 - id = tablename_fieldname 80 - class = field.type 81 - name = fieldname 82 83 :param field: the field needing the widget 84 :param value: value 85 :param attributes: any other attributes to be applied 86 """ 87 88 raise NotImplementedError
89
90 -class StringWidget(FormWidget):
91 92 @staticmethod
93 - def widget(field, value, **attributes):
94 """ 95 generates an INPUT text tag. 96 97 see also: :meth:`FormWidget.widget` 98 """ 99 100 default = dict( 101 _type = 'text', 102 value = (value!=None and str(value)) or '', 103 ) 104 attr = StringWidget._attributes(field, default, **attributes) 105 106 return INPUT(**attr)
107 108
109 -class IntegerWidget(StringWidget):
110 111 pass
112 113
114 -class DoubleWidget(StringWidget):
115 116 pass
117 118
119 -class DecimalWidget(StringWidget):
120 121 pass
122 123
124 -class TimeWidget(StringWidget):
125 126 pass
127 128
129 -class DateWidget(StringWidget):
130 131 pass
132 133
134 -class DatetimeWidget(StringWidget):
135 136 pass
137 138
139 -class TextWidget(FormWidget):
140 141 @staticmethod
142 - def widget(field, value, **attributes):
143 """ 144 generates a TEXTAREA tag. 145 146 see also: :meth:`FormWidget.widget` 147 """ 148 149 default = dict( 150 value = value, 151 ) 152 attr = TextWidget._attributes(field, default, **attributes) 153 154 return TEXTAREA(**attr)
155 156
157 -class BooleanWidget(FormWidget):
158 159 @staticmethod
160 - def widget(field, value, **attributes):
161 """ 162 generates an INPUT checkbox tag. 163 164 see also: :meth:`FormWidget.widget` 165 """ 166 167 default=dict( 168 _type='checkbox', 169 value=value, 170 ) 171 attr = BooleanWidget._attributes(field, default, **attributes) 172 173 return INPUT(**attr)
174 175
176 -class OptionsWidget(FormWidget):
177 178 @staticmethod
179 - def has_options(field):
180 """ 181 checks if the field has selectable options 182 183 :param field: the field needing checking 184 :returns: True if the field has options 185 """ 186 187 return hasattr(field.requires, 'options')
188 189 @staticmethod
190 - def widget(field, value, **attributes):
191 """ 192 generates a SELECT tag, including OPTIONs (only 1 option allowed) 193 194 see also: :meth:`FormWidget.widget` 195 """ 196 default = dict( 197 value=value, 198 ) 199 attr = OptionsWidget._attributes(field, default, **attributes) 200 201 requires = field.requires 202 if not isinstance(requires, (list, tuple)): 203 requires = [requires] 204 if requires: 205 if hasattr(requires[0], 'options'): 206 options = requires[0].options() 207 else: 208 raise SyntaxError, 'widget cannot determine options of %s' \ 209 % field 210 opts = [OPTION(v, _value=k) for (k, v) in options] 211 212 return SELECT(*opts, **attr)
213
214 -class ListWidget(StringWidget):
215 @staticmethod
216 - def widget(field,value,**attributes):
217 _id = '%s_%s' % (field._tablename, field.name) 218 _name = field.name 219 if field.type=='list:integer': _class = 'integer' 220 else: _class = 'string' 221 items=[LI(INPUT(_id=_id,_class=_class,_name=_name,value=v,hideerror=True)) \ 222 for v in value or ['']] 223 script=SCRIPT(""" 224 // from http://refactormycode.com/codes/694-expanding-input-list-using-jquery 225 (function(){ 226 jQuery.fn.grow_input = function() { 227 return this.each(function() { 228 var ul = this; 229 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) }); 230 }); 231 }; 232 function pe(ul) { 233 var new_line = ml(ul); 234 rel(ul); 235 new_line.appendTo(ul); 236 new_line.find(":text").focus(); 237 return false; 238 } 239 function ml(ul) { 240 var line = jQuery(ul).find("li:first").clone(true); 241 line.find(':text').val(''); 242 return line; 243 } 244 function rel(ul) { 245 jQuery(ul).find("li").each(function() { 246 var trimmed = jQuery.trim(jQuery(this.firstChild).val()); 247 if (trimmed=='') jQuery(this).remove(); else jQuery(this.firstChild).val(trimmed); 248 }); 249 } 250 })(); 251 jQuery(document).ready(function(){jQuery('#%s_grow_input').grow_input();}); 252 """ % _id) 253 attributes['_id']=_id+'_grow_input' 254 return TAG[''](UL(*items,**attributes),script)
255 256
257 -class MultipleOptionsWidget(OptionsWidget):
258 259 @staticmethod
260 - def widget(field, value, size=5, **attributes):
261 """ 262 generates a SELECT tag, including OPTIONs (multiple options allowed) 263 264 see also: :meth:`FormWidget.widget` 265 266 :param size: optional param (default=5) to indicate how many rows must 267 be shown 268 """ 269 270 attributes.update(dict(_size=size, _multiple=True)) 271 272 return OptionsWidget.widget(field, value, **attributes)
273 274
275 -class RadioWidget(OptionsWidget):
276 277 @staticmethod
278 - def widget(field, value, **attributes):
279 """ 280 generates a TABLE tag, including INPUT radios (only 1 option allowed) 281 282 see also: :meth:`FormWidget.widget` 283 """ 284 285 attr = OptionsWidget._attributes(field, {}, **attributes) 286 287 requires = field.requires 288 if not isinstance(requires, (list, tuple)): 289 requires = [requires] 290 if requires: 291 if hasattr(requires[0], 'options'): 292 options = requires[0].options() 293 else: 294 raise SyntaxError, 'widget cannot determine options of %s' \ 295 % field 296 297 options = [(k, v) for k, v in options if str(v)] 298 opts = [] 299 cols = attributes.get('cols',1) 300 totals = len(options) 301 mods = totals%cols 302 rows = totals/cols 303 if mods: 304 rows += 1 305 306 for r_index in range(rows): 307 tds = [] 308 for k, v in options[r_index*cols:(r_index+1)*cols]: 309 tds.append(TD(INPUT(_type='radio', _name=field.name, 310 requires=attr.get('requires',None), 311 hideerror=True, _value=k, 312 value=value), v)) 313 opts.append(TR(tds)) 314 315 if opts: 316 opts[-1][0][0]['hideerror'] = False 317 return TABLE(*opts, **attr)
318 319
320 -class CheckboxesWidget(OptionsWidget):
321 322 @staticmethod
323 - def widget(field, value, **attributes):
324 """ 325 generates a TABLE tag, including INPUT checkboxes (multiple allowed) 326 327 see also: :meth:`FormWidget.widget` 328 """ 329 330 # was values = re.compile('[\w\-:]+').findall(str(value)) 331 if isinstance(value, (list, tuple)): 332 values = [str(v) for v in value] 333 else: 334 values = [str(value)] 335 336 attr = OptionsWidget._attributes(field, {}, **attributes) 337 338 requires = field.requires 339 if not isinstance(requires, (list, tuple)): 340 requires = [requires] 341 if requires: 342 if hasattr(requires[0], 'options'): 343 options = requires[0].options() 344 else: 345 raise SyntaxError, 'widget cannot determine options of %s' \ 346 % field 347 348 options = [(k, v) for k, v in options if k!=''] 349 opts = [] 350 cols = attributes.get('cols',1) 351 totals = len(options) 352 mods = totals%cols 353 rows = totals/cols 354 if mods: 355 rows += 1 356 357 for r_index in range(rows): 358 tds = [] 359 for k, v in options[r_index*cols:(r_index+1)*cols]: 360 if int(k) in values: 361 r_value = k 362 else: 363 r_value = [] 364 tds.append(TD(INPUT(_type='checkbox', _name=field.name, 365 requires=attr.get('requires',None), 366 hideerror=True, _value=k, 367 value=r_value), v)) 368 opts.append(TR(tds)) 369 370 if opts: 371 opts[-1][0][0]['hideerror'] = False 372 return TABLE(*opts, **attr)
373 374
375 -class PasswordWidget(FormWidget):
376 377 DEFAULT_PASSWORD_DISPLAY = 8*('*') 378 379 @staticmethod
380 - def widget(field, value, **attributes):
381 """ 382 generates a INPUT password tag. 383 If a value is present it will be shown as a number of '*', not related 384 to the length of the actual value. 385 386 see also: :meth:`FormWidget.widget` 387 """ 388 389 default=dict( 390 _type='password', 391 _value=(value and PasswordWidget.DEFAULT_PASSWORD_DISPLAY) or '', 392 ) 393 attr = PasswordWidget._attributes(field, default, **attributes) 394 395 return INPUT(**attr)
396 397
398 -class UploadWidget(FormWidget):
399 400 DEFAULT_WIDTH = '150px' 401 ID_DELETE_SUFFIX = '__delete' 402 GENERIC_DESCRIPTION = 'file' 403 DELETE_FILE = 'delete' 404 405 @staticmethod
406 - def widget(field, value, download_url=None, **attributes):
407 """ 408 generates a INPUT file tag. 409 410 Optionally provides an A link to the file, including a checkbox so 411 the file can be deleted. 412 All is wrapped in a DIV. 413 414 see also: :meth:`FormWidget.widget` 415 416 :param download_url: Optional URL to link to the file (default = None) 417 """ 418 419 default=dict( 420 _type='file', 421 ) 422 attr = UploadWidget._attributes(field, default, **attributes) 423 424 inp = INPUT(**attr) 425 426 if download_url and value: 427 url = download_url + '/' + value 428 (br, image) = ('', '') 429 if UploadWidget.is_image(value): 430 br = BR() 431 image = IMG(_src = url, _width = UploadWidget.DEFAULT_WIDTH) 432 433 requires = attr["requires"] 434 if requires == [] or isinstance(requires, IS_EMPTY_OR): 435 inp = DIV(inp, '[', 436 A(UploadWidget.GENERIC_DESCRIPTION, _href = url), 437 '|', 438 INPUT(_type='checkbox', 439 _name=field.name + UploadWidget.ID_DELETE_SUFFIX), 440 UploadWidget.DELETE_FILE, 441 ']', br, image) 442 else: 443 inp = DIV(inp, '[', 444 A(UploadWidget.GENERIC_DESCRIPTION, _href = url), 445 ']', br, image) 446 return inp
447 448 @staticmethod
449 - def represent(field, value, download_url=None):
450 """ 451 how to represent the file: 452 453 - with download url and if it is an image: <A href=...><IMG ...></A> 454 - otherwise with download url: <A href=...>file</A> 455 - otherwise: file 456 457 :param field: the field 458 :param value: the field value 459 :param download_url: url for the file download (default = None) 460 """ 461 462 inp = UploadWidget.GENERIC_DESCRIPTION 463 464 if download_url and value: 465 url = download_url + '/' + value 466 if UploadWidget.is_image(value): 467 inp = IMG(_src = url, _width = UploadWidget.DEFAULT_WIDTH) 468 inp = A(inp, _href = url) 469 470 return inp
471 472 @staticmethod
473 - def is_image(value):
474 """ 475 Tries to check if the filename provided references to an image 476 477 Checking is based on filename extension. Currently recognized: 478 gif, png, jp(e)g, bmp 479 480 :param value: filename 481 """ 482 483 extension = value.split('.')[-1].lower() 484 if extension in ['gif', 'png', 'jpg', 'jpeg', 'bmp']: 485 return True 486 return False
487 488
489 -class AutocompleteWidget(object):
490
491 - def __init__(self, request, field, id_field=None, db=None, 492 orderby=None, limitby=(0,10), 493 keyword='_autocomplete_%(fieldname)s', 494 min_length=2):
495 self.request = request 496 self.keyword = keyword % dict(fieldname=field.name) 497 self.db = db or field._db 498 self.orderby = orderby 499 self.limitby = limitby 500 self.min_length = min_length 501 self.fields=[field] 502 if id_field: 503 self.is_reference = True 504 self.fields.append(id_field) 505 else: 506 self.is_reference = False 507 if hasattr(request,'application'): 508 self.url = Url(r=request, args=request.args) 509 self.callback() 510 else: 511 self.url = request
512 - def callback(self):
513 if self.keyword in self.request.vars: 514 field = self.fields[0] 515 rows = self.db(field.like(self.request.vars[self.keyword]+'%'))\ 516 .select(orderby=self.orderby,limitby=self.limitby,*self.fields) 517 if rows: 518 if self.is_reference: 519 id_field = self.fields[1] 520 raise HTTP(200,SELECT(_id=self.keyword,_class='autocomplete', 521 _size=len(rows),_multiple=(len(rows)==1), 522 *[OPTION(s[field.name],_value=s[id_field.name], 523 _selected=(k==0)) \ 524 for k,s in enumerate(rows)]).xml()) 525 else: 526 raise HTTP(200,SELECT(_id=self.keyword,_class='autocomplete', 527 _size=len(rows),_multiple=(len(rows)==1), 528 *[OPTION(s[field.name], 529 _selected=(k==0)) \ 530 for k,s in enumerate(rows)]).xml()) 531 else: 532 533 raise HTTP(200,'')
534 - def __call__(self,field,value,**attributes):
535 default = dict( 536 _type = 'text', 537 value = (value!=None and str(value)) or '', 538 ) 539 attr = StringWidget._attributes(field, default, **attributes) 540 div_id = self.keyword+'_div' 541 attr['_autocomplete']='off' 542 if self.is_reference: 543 key2 = self.keyword+'_aux' 544 key3 = self.keyword+'_auto' 545 attr['_class']='string' 546 name = attr['_name'] 547 if 'requires' in attr: del attr['requires'] 548 attr['_name'] = key2 549 value = attr['value'] 550 record = self.db(self.fields[1]==value).select(self.fields[0]).first() 551 attr['value'] = record and record[self.fields[0].name] 552 attr['_onblur']="jQuery('#%(div_id)s').delay(3000).fadeOut('slow');" % \ 553 dict(div_id=div_id,u='F'+self.keyword) 554 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');" % \ 555 dict(url=self.url,min_length=self.min_length, 556 key=self.keyword,id=attr['_id'],key2=key2,key3=key3, 557 name=name,div_id=div_id,u='F'+self.keyword) 558 if self.min_length==0: 559 attr['_onfocus'] = attr['_onkeyup'] 560 return TAG[''](INPUT(**attr),INPUT(_type='hidden',_id=key3,_value=value, 561 _name=name,requires=field.requires), 562 DIV(_id=div_id,_style='position:absolute;')) 563 else: 564 attr['_name']=field.name 565 attr['_onblur']="jQuery('#%(div_id)s').delay(3000).fadeOut('slow');" % \ 566 dict(div_id=div_id,u='F'+self.keyword) 567 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');" % \ 568 dict(url=self.url,min_length=self.min_length, 569 key=self.keyword,id=attr['_id'],div_id=div_id,u='F'+self.keyword) 570 if self.min_length==0: 571 attr['_onfocus'] = attr['_onkeyup'] 572 return TAG[''](INPUT(**attr),DIV(_id=div_id,_style='position:absolute;'))
573 574
575 -class SQLFORM(FORM):
576 577 """ 578 SQLFORM is used to map a table (and a current record) into an HTML form 579 580 given a SQLTable stored in db.table 581 582 generates an insert form:: 583 584 SQLFORM(db.table) 585 586 generates an update form:: 587 588 record=db.table[some_id] 589 SQLFORM(db.table, record) 590 591 generates an update with a delete button:: 592 593 SQLFORM(db.table, record, deletable=True) 594 595 if record is an int:: 596 597 record=db.table[record] 598 599 optional arguments: 600 601 :param fields: a list of fields that should be placed in the form, 602 default is all. 603 :param labels: a dictionary with labels for each field, keys are the field 604 names. 605 :param col3: a dictionary with content for an optional third column 606 (right of each field). keys are field names. 607 :param linkto: the URL of a controller/function to access referencedby 608 records 609 see controller appadmin.py for examples 610 :param upload: the URL of a controller/function to download an uploaded file 611 see controller appadmin.py for examples 612 613 any named optional attribute is passed to the <form> tag 614 for example _class, _id, _style, _action, _method, etc. 615 616 """ 617 618 # usability improvements proposal by fpp - 4 May 2008 : 619 # - correct labels (for points to field id, not field name) 620 # - add label for delete checkbox 621 # - add translatable label for record ID 622 # - add third column to right of fields, populated from the col3 dict 623 624 widgets = Storage(dict( 625 string = StringWidget, 626 text = TextWidget, 627 password = PasswordWidget, 628 integer = IntegerWidget, 629 double = DoubleWidget, 630 decimal = DecimalWidget, 631 time = TimeWidget, 632 date = DateWidget, 633 datetime = DatetimeWidget, 634 upload = UploadWidget, 635 boolean = BooleanWidget, 636 blob = None, 637 options = OptionsWidget, 638 multiple = MultipleOptionsWidget, 639 radio = RadioWidget, 640 checkboxes = CheckboxesWidget, 641 autocomplete = AutocompleteWidget, 642 list = ListWidget, 643 )) 644 645 FIELDNAME_REQUEST_DELETE = 'delete_this_record' 646 FIELDKEY_DELETE_RECORD = 'delete_record' 647 ID_LABEL_SUFFIX = '__label' 648 ID_ROW_SUFFIX = '__row' 649
650 - def __init__( 651 self, 652 table, 653 record = None, 654 deletable = False, 655 linkto = None, 656 upload = None, 657 fields = None, 658 labels = None, 659 col3 = {}, 660 submit_button = 'Submit', 661 delete_label = 'Check to delete:', 662 showid = True, 663 readonly = False, 664 comments = True, 665 keepopts = [], 666 ignore_rw = False, 667 record_id = None, 668 formstyle = 'table3cols', 669 buttons = ['submit'], 670 **attributes 671 ):
672 """ 673 SQLFORM(db.table, 674 record=None, 675 fields=['name'], 676 labels={'name': 'Your name'}, 677 linkto=URL(r=request, f='table/db/') 678 """ 679 680 self.ignore_rw = ignore_rw 681 self.formstyle = formstyle 682 nbsp = XML('&nbsp;') # Firefox2 does not display fields with blanks 683 FORM.__init__(self, *[], **attributes) 684 ofields = fields 685 keyed = hasattr(table,'_primarykey') 686 687 # if no fields are provided, build it from the provided table 688 # will only use writable or readable fields, unless forced to ignore 689 if fields == None: 690 fields = [f.name for f in table if (ignore_rw or f.writable or f.readable) and not f.compute] 691 self.fields = fields 692 693 # make sure we have an id 694 if self.fields[0] != table.fields[0] and \ 695 isinstance(table,Table) and not keyed: 696 self.fields.insert(0, table.fields[0]) 697 698 self.table = table 699 700 # try to retrieve the indicated record using its id 701 # otherwise ignore it 702 if record and isinstance(record, (int, long, str, unicode)): 703 if not str(record).isdigit(): 704 raise HTTP(404, "Object not found") 705 record = table._db(table.id == record).select().first() 706 if not record: 707 raise HTTP(404, "Object not found") 708 self.record = record 709 710 self.record_id = record_id 711 if keyed: 712 if record: 713 self.record_id = dict([(k,record[k]) for k in table._primarykey]) 714 else: 715 self.record_id = dict([(k,None) for k in table._primarykey]) 716 self.field_parent = {} 717 xfields = [] 718 self.fields = fields 719 self.custom = Storage() 720 self.custom.dspval = Storage() 721 self.custom.inpval = Storage() 722 self.custom.label = Storage() 723 self.custom.comment = Storage() 724 self.custom.widget = Storage() 725 self.custom.linkto = Storage() 726 727 for fieldname in self.fields: 728 if fieldname.find('.') >= 0: 729 continue 730 731 field = self.table[fieldname] 732 comment = None 733 734 if comments: 735 comment = col3.get(fieldname, field.comment) 736 if comment == None: 737 comment = '' 738 self.custom.comment[fieldname] = comment 739 740 if labels != None and fieldname in labels: 741 label = labels[fieldname] 742 colon = '' 743 else: 744 label = field.label 745 colon = ': ' 746 self.custom.label[fieldname] = label 747 748 field_id = '%s_%s' % (table._tablename, fieldname) 749 750 label = LABEL(label, colon, _for=field_id, 751 _id=field_id+SQLFORM.ID_LABEL_SUFFIX) 752 753 row_id = field_id+SQLFORM.ID_ROW_SUFFIX 754 if field.type == 'id': 755 self.custom.dspval.id = nbsp 756 self.custom.inpval.id = '' 757 widget = '' 758 if record: 759 if showid and 'id' in fields and field.readable: 760 v = record['id'] 761 widget = SPAN(v, _id=field_id) 762 self.custom.dspval.id = str(v) 763 xfields.append((row_id,label, widget,comment)) 764 self.record_id = str(record['id']) 765 self.custom.widget.id = widget 766 continue 767 768 if readonly and not ignore_rw and not field.readable: 769 continue 770 771 if record: 772 default = record[fieldname] 773 else: 774 default = field.default 775 if isinstance(default,CALLABLETYPES): 776 default=default() 777 778 cond = readonly or \ 779 (not ignore_rw and not field.writable and field.readable) 780 781 if default and not cond: 782 default = field.formatter(default) 783 dspval = default 784 inpval = default 785 786 if cond: 787 788 # ## if field.represent is available else 789 # ## ignore blob and preview uploaded images 790 # ## format everything else 791 792 if field.represent: 793 inp = field.represent(default) 794 elif field.type in ['blob']: 795 continue 796 elif field.type == 'upload': 797 inp = UploadWidget.represent(field, default, upload) 798 elif field.type == 'boolean': 799 inp = self.widgets.boolean.widget(field, default, _disabled=True) 800 else: 801 inp = field.formatter(default) 802 elif field.type == 'upload': 803 if hasattr(field, 'widget') and field.widget: 804 inp = field.widget(field, default, upload) 805 else: 806 inp = self.widgets.upload.widget(field, default, upload) 807 elif hasattr(field, 'widget') and field.widget: 808 inp = field.widget(field, default) 809 elif field.type == 'boolean': 810 inp = self.widgets.boolean.widget(field, default) 811 if default: 812 inpval = 'checked' 813 else: 814 inpval = '' 815 elif OptionsWidget.has_options(field): 816 if not field.requires.multiple: 817 inp = self.widgets.options.widget(field, default) 818 else: 819 inp = self.widgets.multiple.widget(field, default) 820 if fieldname in keepopts: 821 inpval = TAG[''](*inp.components) 822 elif field.type.startswith('list:'): 823 inp = self.widgets.list.widget(field,default) 824 elif field.type == 'text': 825 inp = self.widgets.text.widget(field, default) 826 elif field.type == 'password': 827 inp = self.widgets.password.widget(field, default) 828 if self.record: 829 dspval = PasswordWidget.DEFAULT_PASSWORD_DISPLAY 830 else: 831 dspval = '' 832 elif field.type == 'blob': 833 continue 834 else: 835 inp = self.widgets.string.widget(field, default) 836 837 xfields.append((row_id,label,inp,comment)) 838 self.custom.dspval[fieldname] = dspval or nbsp 839 self.custom.inpval[fieldname] = inpval or '' 840 self.custom.widget[fieldname] = inp 841 842 # if a record is provided and found, as is linkto 843 # build a link 844 if record and linkto: 845 db = linkto.split('/')[-1] 846 for (rtable, rfield) in table._referenced_by: 847 if keyed: 848 rfld = table._db[rtable][rfield] 849 query = urllib.quote('%s.%s==%s' % (db,rfld,record[rfld.type[10:].split('.')[1]])) 850 else: 851 # <block> 852 query = urllib.quote('%s.%s==%s' % (db,table._db[rtable][rfield],record.id)) 853 lname = olname = '%s.%s' % (rtable, rfield) 854 if ofields and not olname in ofields: 855 continue 856 if labels and lname in labels: 857 lname = labels[lname] 858 widget = A(lname, 859 _class='reference', 860 _href='%s/%s?query=%s' % (linkto, rtable, query)) 861 xfields.append((olname.replace('.', '__')+SQLFORM.ID_ROW_SUFFIX, 862 '',widget,col3.get(olname,''))) 863 self.custom.linkto[olname.replace('.', '__')] = widget 864 # </block> 865 866 # when deletable, add delete? checkbox 867 self.custom.deletable = '' 868 if record and deletable: 869 widget = INPUT(_type='checkbox', 870 _class='delete', 871 _id=self.FIELDKEY_DELETE_RECORD, 872 _name=self.FIELDNAME_REQUEST_DELETE, 873 ) 874 xfields.append((self.FIELDKEY_DELETE_RECORD+SQLFORM.ID_ROW_SUFFIX, 875 LABEL( 876 delete_label, 877 _for=self.FIELDKEY_DELETE_RECORD, 878 _id=self.FIELDKEY_DELETE_RECORD+SQLFORM.ID_LABEL_SUFFIX), 879 widget, 880 col3.get(self.FIELDKEY_DELETE_RECORD, ''))) 881 self.custom.deletable = widget 882 # when writable, add submit button 883 self.custom.submit = '' 884 if (not readonly) and ('submit' in buttons): 885 widget = INPUT(_type='submit', 886 _value=submit_button) 887 xfields.append(('submit_record'+SQLFORM.ID_ROW_SUFFIX, 888 '', widget,col3.get('submit_button', ''))) 889 self.custom.submit = widget 890 # if a record is provided and found 891 # make sure it's id is stored in the form 892 if record: 893 if not self['hidden']: 894 self['hidden'] = {} 895 if not keyed: 896 self['hidden']['id'] = record['id'] 897 898 (begin, end) = self._xml() 899 self.custom.begin = XML("<%s %s>" % (self.tag, begin)) 900 self.custom.end = XML("%s</%s>" % (end, self.tag)) 901 table = self.createform(xfields) 902 self.components = [table]
903
904 - def createform(self, xfields):
905 if self.formstyle == 'table3cols': 906 table = TABLE() 907 for id,a,b,c in xfields: 908 td_b = self.field_parent[id] = TD(b,_class='w2p_fw') 909 table.append(TR(TD(a,_class='w2p_fl'), 910 td_b, 911 TD(c,_class='w2p_fc'),_id=id)) 912 elif self.formstyle == 'table2cols': 913 table = TABLE() 914 for id,a,b,c in xfields: 915 td_b = self.field_parent[id] = TD(b,_class='w2p_fw',_colspan="2") 916 table.append(TR(TD(a,_class='w2p_fl'), 917 TD(c,_class='w2p_fc'),_id=id 918 +'1',_class='even')) 919 table.append(TR(td_b,_id=id+'2',_class='odd')) 920 elif self.formstyle == 'divs': 921 table = TAG['']() 922 for id,a,b,c in xfields: 923 div_b = self.field_parent[id] = DIV(b,_class='w2p_fw') 924 table.append(DIV(DIV(a,_class='w2p_fl'), 925 div_b, 926 DIV(c,_class='w2p_fc'),_id=id)) 927 elif self.formstyle == 'ul': 928 table = UL() 929 for id,a,b,c in xfields: 930 div_b = self.field_parent[id] = DIV(b,_class='w2p_fw') 931 table.append(LI(DIV(a,_class='w2p_fl'), 932 div_b, 933 DIV(c,_class='w2p_fc'),_id=id)) 934 elif type(self.formstyle) == type(lambda:None): 935 table = TABLE() 936 for id,a,b,c in xfields: 937 td_b = self.field_parent[id] = TD(b,_class='w2p_fw') 938 newrows = self.formstyle(id,a,td_b,c) 939 if type(newrows).__name__ != "tuple": 940 newrows = [newrows] 941 for newrow in newrows: 942 table.append(newrow) 943 else: 944 raise RuntimeError, 'formsyle not supported' 945 return table
946 947
948 - def accepts( 949 self, 950 request_vars, 951 session=None, 952 formname='%(tablename)s_%(record_id)s', 953 keepvalues=False, 954 onvalidation=None, 955 dbio=True, 956 hideerror=False, 957 detect_record_change=False, 958 ):
959 960 """ 961 similar FORM.accepts but also does insert, update or delete in DAL. 962 but if detect_record_change == True than: 963 form.record_changed = False (record is properly validated/submitted) 964 form.record_changed = True (record cannot be submitted because changed) 965 elseif detect_record_change == False than: 966 form.record_changed = None 967 """ 968 969 if request_vars.__class__.__name__ == 'Request': 970 request_vars = request_vars.post_vars 971 972 keyed = hasattr(self.table, '_primarykey') 973 974 # implement logic to detect whether record exist but has been modified 975 # server side 976 self.record_changed = None 977 if detect_record_change: 978 if self.record: 979 self.record_changed = False 980 serialized = '|'.join(str(self.record[k]) for k in self.table.fields()) 981 self.record_hash = md5_hash(serialized) 982 983 # logic to deal with record_id for keyed tables 984 if self.record: 985 if keyed: 986 formname_id = '.'.join(str(self.record[k]) 987 for k in self.table._primarykey 988 if hasattr(self.record,k)) 989 record_id = dict((k, request_vars[k]) for k in self.table._primarykey) 990 else: 991 (formname_id, record_id) = (self.record.id, 992 request_vars.get('id', None)) 993 keepvalues = True 994 else: 995 if keyed: 996 formname_id = 'create' 997 record_id = dict([(k, None) for k in self.table._primarykey]) 998 else: 999 (formname_id, record_id) = ('create', None) 1000 1001 if not keyed and isinstance(record_id, (list, tuple)): 1002 record_id = record_id[0] 1003 1004 if formname: 1005 formname = formname % dict(tablename = self.table._tablename, 1006 record_id = formname_id) 1007 1008 # ## THIS IS FOR UNIQUE RECORDS, read IS_NOT_IN_DB 1009 1010 for fieldname in self.fields: 1011 field = self.table[fieldname] 1012 requires = field.requires or [] 1013 if not isinstance(requires, (list, tuple)): 1014 requires = [requires] 1015 [item.set_self_id(self.record_id) for item in requires 1016 if hasattr(item, 'set_self_id') and self.record_id] 1017 1018 # ## END 1019 1020 fields = {} 1021 for key in self.vars: 1022 fields[key] = self.vars[key] 1023 1024 ret = FORM.accepts( 1025 self, 1026 request_vars, 1027 session, 1028 formname, 1029 keepvalues, 1030 onvalidation, 1031 hideerror=hideerror, 1032 ) 1033 1034 if not ret and self.record and self.errors: 1035 ### if there are errors in update mode 1036 # and some errors refers to an already uploaded file 1037 # delete error if 1038 # - user not trying to upload a new file 1039 # - there is existing file and user is not trying to delete it 1040 # this is because removing the file may not pass validation 1041 for key in self.errors.keys(): 1042 if self.table[key].type == 'upload' \ 1043 and request_vars.get(key, None) in (None, '') \ 1044 and self.record[key] \ 1045 and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars: 1046 del self.errors[key] 1047 if not self.errors: 1048 ret = True 1049 1050 requested_delete = \ 1051 request_vars.get(self.FIELDNAME_REQUEST_DELETE, False) 1052 1053 self.custom.end = TAG[''](self.hidden_fields(), self.custom.end) 1054 1055 auch = record_id and self.errors and requested_delete 1056 1057 # auch is true when user tries to delete a record 1058 # that does not pass validation, yet it should be deleted 1059 1060 if not ret and not auch: 1061 for fieldname in self.fields: 1062 field = self.table[fieldname] 1063 ### this is a workaround! widgets should always have default not None! 1064 if not field.widget and field.type.startswith('list:') and \ 1065 not OptionsWidget.has_options(field): 1066 field.widget = self.widgets.list.widget 1067 if hasattr(field, 'widget') and field.widget and fieldname in request_vars: 1068 if fieldname in self.vars: 1069 value = self.vars[fieldname] 1070 elif self.record: 1071 value = self.record[fieldname] 1072 else: 1073 value = self.table[fieldname].default 1074 row_id = '%s_%s%s' % (self.table, fieldname, SQLFORM.ID_ROW_SUFFIX) 1075 widget = field.widget(field, value) 1076 self.field_parent[row_id].components = [ widget ] 1077 if not field.type.startswith('list:'): 1078 self.field_parent[row_id]._traverse(False, hideerror) 1079 self.custom.widget[ fieldname ] = widget 1080 return ret 1081 1082 if record_id and str(record_id) != str(self.record_id): 1083 raise SyntaxError, 'user is tampering with form\'s record_id: ' \ 1084 '%s != %s' % (record_id, self.record_id) 1085 1086 if record_id and dbio: 1087 if keyed: 1088 self.vars.update(record_id) 1089 else: 1090 self.vars.id = self.record.id 1091 1092 if requested_delete and self.custom.deletable: 1093 if dbio: 1094 if keyed: 1095 qry = reduce(lambda x, y: x & y, 1096 [self.table[k] == record_id[k] for k in self.table._primarykey]) 1097 else: 1098 qry = self.table.id == self.record.id 1099 self.table._db(qry).delete() 1100 self.errors.clear() 1101 for component in self.elements('input, select, textarea'): 1102 component['_disabled'] = True 1103 return True 1104 1105 for fieldname in self.fields: 1106 if not fieldname in self.table.fields: 1107 continue 1108 1109 if not self.ignore_rw and not self.table[fieldname].writable: 1110 ### this happens because FORM has no knowledge of writable 1111 ### and thinks that a missing boolean field is a None 1112 if self.table[fieldname].type == 'boolean' and \ 1113 self.vars.get(fieldname, True) == None: 1114 del self.vars[fieldname] 1115 continue 1116 1117 field = self.table[fieldname] 1118 if field.type == 'id': 1119 continue 1120 if field.type == 'boolean': 1121 if self.vars.get(fieldname, False): 1122 self.vars[fieldname] = fields[fieldname] = True 1123 else: 1124 self.vars[fieldname] = fields[fieldname] = False 1125 elif field.type == 'password' and self.record\ 1126 and request_vars.get(fieldname, None) == \ 1127 PasswordWidget.DEFAULT_PASSWORD_DISPLAY: 1128 continue # do not update if password was not changed 1129 elif field.type == 'upload': 1130 f = self.vars[fieldname] 1131 fd = '%s__delete' % fieldname 1132 if f == '' or f == None: 1133 if self.vars.get(fd, False) or not self.record: 1134 fields[fieldname] = '' 1135 else: 1136 fields[fieldname] = self.record[fieldname] 1137 self.vars[fieldname] = fields[fieldname] 1138 continue 1139 elif hasattr(f, 'file'): 1140 (source_file, original_filename) = (f.file, f.filename) 1141 elif isinstance(f, (str, unicode)): 1142 ### do not know why this happens, it should not 1143 (source_file, original_filename) = \ 1144 (cStringIO.StringIO(f), 'file.txt') 1145 newfilename = field.store(source_file, original_filename) 1146 # this line is for backward compatibility only 1147 self.vars['%s_newfilename' % fieldname] = newfilename 1148 fields[fieldname] = newfilename 1149 if isinstance(field.uploadfield, str): 1150 fields[field.uploadfield] = source_file.read() 1151 # proposed by Hamdy (accept?) do we need fields at this point? 1152 self.vars[fieldname] = fields[fieldname] 1153 continue 1154 elif fieldname in self.vars: 1155 fields[fieldname] = self.vars[fieldname] 1156 elif field.default == None and field.type != 'blob': 1157 self.errors[fieldname] = 'no data' 1158 return False 1159 value = fields.get(fieldname,None) 1160 if field.type == 'list:string': 1161 if not isinstance(value, (tuple, list)): 1162 fields[fieldname] = value and [value] or [] 1163 elif field.type.startswith('list:'): 1164 if not isinstance(value, list): 1165 fields[fieldname] = [safe_int(x) for x in (value and [value] or [])] 1166 elif field.type == 'integer': 1167 if value != None: 1168 fields[fieldname] = safe_int(value) 1169 elif field.type.startswith('reference'): 1170 if value != None and isinstance(self.table, Table) and not keyed: 1171 fields[fieldname] = safe_int(value) 1172 elif field.type == 'double': 1173 if value != None: 1174 fields[fieldname] = safe_float(value) 1175 1176 for fieldname in self.vars: 1177 if fieldname != 'id' and fieldname in self.table.fields\ 1178 and not fieldname in fields and not fieldname\ 1179 in request_vars: 1180 fields[fieldname] = self.vars[fieldname] 1181 1182 if dbio: 1183 if keyed: 1184 if reduce(lambda x, y: x and y, record_id.values()): # if record_id 1185 if fields: 1186 qry = reduce(lambda x, y: x & y, 1187 [self.table[k] == self.record[k] for k in self.table._primarykey]) 1188 self.table._db(qry).update(**fields) 1189 else: 1190 pk = self.table.insert(**fields) 1191 if pk: 1192 self.vars.update(pk) 1193 else: 1194 ret = False 1195 else: 1196 if record_id: 1197 self.vars.id = self.record.id 1198 if fields: 1199 self.table._db(self.table.id == self.record.id).update(**fields) 1200 else: 1201 self.vars.id = self.table.insert(**fields) 1202 return ret
1203 1204 @staticmethod
1205 - def factory(*fields, **attributes):
1206 """ 1207 generates a SQLFORM for the given fields. 1208 1209 Internally will build a non-database based data model 1210 to hold the fields. 1211 """ 1212 # Define a table name, this way it can be logical to our CSS. 1213 # And if you switch from using SQLFORM to SQLFORM.factory 1214 # your same css definitions will still apply. 1215 1216 table_name = attributes.get('table_name', 'no_table') 1217 1218 # So it won't interfear with SQLDB.define_table 1219 if 'table_name' in attributes: 1220 del attributes['table_name'] 1221 1222 return SQLFORM(DAL(None).define_table(table_name, *fields), **attributes)
1223 1224
1225 -class SQLTABLE(TABLE):
1226 1227 """ 1228 given a Rows object, as returned by a db().select(), generates 1229 an html table with the rows. 1230 1231 optional arguments: 1232 1233 :param linkto: URL (or lambda to generate a URL) to edit individual records 1234 :param upload: URL to download uploaded files 1235 :param orderby: Add an orderby link to column headers. 1236 :param headers: dictionary of headers to headers redefinions 1237 headers can also be a string to gerenare the headers from data 1238 for now only headers="fieldname:capitalize", 1239 headers="labels" and headers=None are supported 1240 :param truncate: length at which to truncate text in table cells. 1241 Defaults to 16 characters. 1242 :param columns: a list or dict contaning the names of the columns to be shown 1243 Defaults to all 1244 1245 Optional names attributes for passed to the <table> tag 1246 1247 The keys of headers and columns must be of the form "tablename.fieldname" 1248 1249 Simple linkto example:: 1250 1251 rows = db.select(db.sometable.ALL) 1252 table = SQLTABLE(rows, linkto='someurl') 1253 1254 This will link rows[id] to .../sometable/value_of_id 1255 1256 1257 More advanced linkto example:: 1258 1259 def mylink(field, type, ref): 1260 return URL(r=request, args=[field]) 1261 1262 rows = db.select(db.sometable.ALL) 1263 table = SQLTABLE(rows, linkto=mylink) 1264 1265 This will link rows[id] to 1266 current_app/current_controlle/current_function/value_of_id 1267 1268 1269 """ 1270
1271 - def __init__( 1272 self, 1273 sqlrows, 1274 linkto=None, 1275 upload=None, 1276 orderby=None, 1277 headers={}, 1278 truncate=16, 1279 columns=None, 1280 th_link='', 1281 **attributes 1282 ):
1283 1284 TABLE.__init__(self, **attributes) 1285 self.components = [] 1286 self.attributes = attributes 1287 self.sqlrows = sqlrows 1288 (components, row) = (self.components, []) 1289 if not columns: 1290 columns = sqlrows.colnames 1291 if headers=='fieldname:capitalize': 1292 headers = {} 1293 for c in columns: 1294 headers[c] = ' '.join([w.capitalize() for w in c.split('.')[-1].split('_')]) 1295 elif headers=='labels': 1296 headers = {} 1297 for c in columns: 1298 (t,f) = c.split('.') 1299 field = sqlrows.db[t][f] 1300 headers[c] = field.label 1301 1302 if headers!=None: 1303 for c in columns: 1304 if orderby: 1305 row.append(TH(A(headers.get(c, c), 1306 _href=th_link+'?orderby=' + c))) 1307 else: 1308 row.append(TH(headers.get(c, c))) 1309 components.append(THEAD(TR(*row))) 1310 1311 tbody = [] 1312 for (rc, record) in enumerate(sqlrows): 1313 row = [] 1314 if rc % 2 == 0: 1315 _class = 'even' 1316 else: 1317 _class = 'odd' 1318 for colname in columns: 1319 if not table_field.match(colname): 1320 if "_extra" in record and colname in record._extra: 1321 r = record._extra[colname] 1322 row.append(TD(r)) 1323 continue 1324 else: 1325 raise KeyError("Column %s not found (SQLTABLE)" % colname) 1326 (tablename, fieldname) = colname.split('.') 1327 try: 1328 field = sqlrows.db[tablename][fieldname] 1329 except KeyError: 1330 field = None 1331 if tablename in record \ 1332 and isinstance(record,Row) \ 1333 and isinstance(record[tablename],Row): 1334 r = record[tablename][fieldname] 1335 elif fieldname in record: 1336 r = record[fieldname] 1337 else: 1338 raise SyntaxError, 'something wrong in Rows object' 1339 r_old = r 1340 if not field: 1341 pass 1342 elif linkto and field.type == 'id': 1343 try: 1344 href = linkto(r, 'table', tablename) 1345 except TypeError: 1346 href = '%s/%s/%s' % (linkto, tablename, r_old) 1347 r = A(r, _href=href) 1348 elif field.type.startswith('reference'): 1349 if linkto: 1350 ref = field.type[10:] 1351 try: 1352 href = linkto(r, 'reference', ref) 1353 except TypeError: 1354 href = '%s/%s/%s' % (linkto, ref, r_old) 1355 if ref.find('.') >= 0: 1356 tref,fref = ref.split('.') 1357 if hasattr(sqlrows.db[tref],'_primarykey'): 1358 href = '%s/%s?%s' % (linkto, tref, urllib.urlencode({fref:r})) 1359 if field.represent: 1360 r = A(field.represent(r), _href=str(href)) 1361 else: 1362 r = A(str(r), _href=str(href)) 1363 elif field.represent: 1364 r = field.represent(r) 1365 elif linkto and hasattr(field._table,'_primarykey') and fieldname in field._table._primarykey: 1366 # have to test this with multi-key tables 1367 key = urllib.urlencode(dict( [ \ 1368 ((tablename in record \ 1369 and isinstance(record, Row) \ 1370 and isinstance(record[tablename], Row)) and 1371 (k, record[tablename][k])) or (k, record[k]) \ 1372 for k in field._table._primarykey ] )) 1373 r = A(r, _href='%s/%s?%s' % (linkto, tablename, key)) 1374 elif field.type.startswith('list:'): 1375 r = field.represent(r or []) 1376 elif field.represent: 1377 r = field.represent(r) 1378 elif field.type == 'blob' and r: 1379 r = 'DATA' 1380 elif field.type == 'upload': 1381 if upload and r: 1382 r = A('file', _href='%s/%s' % (upload, r)) 1383 elif r: 1384 r = 'file' 1385 else: 1386 r = '' 1387 elif field.type in ['string','text']: 1388 r = str(field.formatter(r)) 1389 ur = unicode(r, 'utf8') 1390 if truncate!=None and len(ur) > truncate: 1391 r = ur[:truncate - 3].encode('utf8') + '...' 1392 row.append(TD(r)) 1393 tbody.append(TR(_class=_class, *row)) 1394 components.append(TBODY(*tbody))
1395 1396 form_factory = SQLFORM.factory # for backward compatibility, deprecated 1397