| Home | Trees | Indices | Help |
|
|---|
|
|
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
40
46
48 """
49 helper for SQLFORM to generate form input fields (widget),
50 related to the fieldtype
51 """
52
53 @staticmethod
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
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
91
92 @staticmethod
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
112
113
117
118
122
123
127
128
132
133
137
138
140
141 @staticmethod
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
158
159 @staticmethod
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
177
178 @staticmethod
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
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
215 @staticmethod
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
258
259 @staticmethod
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
276
277 @staticmethod
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
321
322 @staticmethod
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 str(k).isdigit() and 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
376
377 DEFAULT_PASSWORD_DISPLAY = 8*('*')
378
379 @staticmethod
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
399
400 DEFAULT_WIDTH = '150px'
401 ID_DELETE_SUFFIX = '__delete'
402 GENERIC_DESCRIPTION = 'file'
403 DELETE_FILE = 'delete'
404
405 @staticmethod
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
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
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
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
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,'')
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
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(' ') # 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
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 key in self.table \
1043 and self.table[key].type == 'upload' \
1044 and request_vars.get(key, None) in (None, '') \
1045 and self.record[key] \
1046 and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars:
1047 del self.errors[key]
1048 if not self.errors:
1049 ret = True
1050
1051 requested_delete = \
1052 request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
1053
1054 self.custom.end = TAG[''](self.hidden_fields(), self.custom.end)
1055
1056 auch = record_id and self.errors and requested_delete
1057
1058 # auch is true when user tries to delete a record
1059 # that does not pass validation, yet it should be deleted
1060
1061 if not ret and not auch:
1062 for fieldname in self.fields:
1063 field = self.table[fieldname]
1064 ### this is a workaround! widgets should always have default not None!
1065 if not field.widget and field.type.startswith('list:') and \
1066 not OptionsWidget.has_options(field):
1067 field.widget = self.widgets.list.widget
1068 if hasattr(field, 'widget') and field.widget and fieldname in request_vars:
1069 if fieldname in self.vars:
1070 value = self.vars[fieldname]
1071 elif self.record:
1072 value = self.record[fieldname]
1073 else:
1074 value = self.table[fieldname].default
1075 row_id = '%s_%s%s' % (self.table, fieldname, SQLFORM.ID_ROW_SUFFIX)
1076 widget = field.widget(field, value)
1077 self.field_parent[row_id].components = [ widget ]
1078 if not field.type.startswith('list:'):
1079 self.field_parent[row_id]._traverse(False, hideerror)
1080 self.custom.widget[ fieldname ] = widget
1081 return ret
1082
1083 if record_id and str(record_id) != str(self.record_id):
1084 raise SyntaxError, 'user is tampering with form\'s record_id: ' \
1085 '%s != %s' % (record_id, self.record_id)
1086
1087 if record_id and dbio:
1088 if keyed:
1089 self.vars.update(record_id)
1090 else:
1091 self.vars.id = self.record.id
1092
1093 if requested_delete and self.custom.deletable:
1094 if dbio:
1095 if keyed:
1096 qry = reduce(lambda x, y: x & y,
1097 [self.table[k] == record_id[k] for k in self.table._primarykey])
1098 else:
1099 qry = self.table.id == self.record.id
1100 self.table._db(qry).delete()
1101 self.errors.clear()
1102 for component in self.elements('input, select, textarea'):
1103 component['_disabled'] = True
1104 return True
1105
1106 for fieldname in self.fields:
1107 if not fieldname in self.table.fields:
1108 continue
1109
1110 if not self.ignore_rw and not self.table[fieldname].writable:
1111 ### this happens because FORM has no knowledge of writable
1112 ### and thinks that a missing boolean field is a None
1113 if self.table[fieldname].type == 'boolean' and \
1114 self.vars.get(fieldname, True) == None:
1115 del self.vars[fieldname]
1116 continue
1117
1118 field = self.table[fieldname]
1119 if field.type == 'id':
1120 continue
1121 if field.type == 'boolean':
1122 if self.vars.get(fieldname, False):
1123 self.vars[fieldname] = fields[fieldname] = True
1124 else:
1125 self.vars[fieldname] = fields[fieldname] = False
1126 elif field.type == 'password' and self.record\
1127 and request_vars.get(fieldname, None) == \
1128 PasswordWidget.DEFAULT_PASSWORD_DISPLAY:
1129 continue # do not update if password was not changed
1130 elif field.type == 'upload':
1131 f = self.vars[fieldname]
1132 fd = '%s__delete' % fieldname
1133 if f == '' or f == None:
1134 if self.vars.get(fd, False) or not self.record:
1135 fields[fieldname] = ''
1136 else:
1137 fields[fieldname] = self.record[fieldname]
1138 self.vars[fieldname] = fields[fieldname]
1139 continue
1140 elif hasattr(f, 'file'):
1141 (source_file, original_filename) = (f.file, f.filename)
1142 elif isinstance(f, (str, unicode)):
1143 ### do not know why this happens, it should not
1144 (source_file, original_filename) = \
1145 (cStringIO.StringIO(f), 'file.txt')
1146 newfilename = field.store(source_file, original_filename)
1147 # this line is for backward compatibility only
1148 self.vars['%s_newfilename' % fieldname] = newfilename
1149 fields[fieldname] = newfilename
1150 if isinstance(field.uploadfield, str):
1151 fields[field.uploadfield] = source_file.read()
1152 # proposed by Hamdy (accept?) do we need fields at this point?
1153 self.vars[fieldname] = fields[fieldname]
1154 continue
1155 elif fieldname in self.vars:
1156 fields[fieldname] = self.vars[fieldname]
1157 elif field.default == None and field.type != 'blob':
1158 self.errors[fieldname] = 'no data'
1159 return False
1160 value = fields.get(fieldname,None)
1161 if field.type == 'list:string':
1162 if not isinstance(value, (tuple, list)):
1163 fields[fieldname] = value and [value] or []
1164 elif field.type.startswith('list:'):
1165 if not isinstance(value, list):
1166 fields[fieldname] = [safe_int(x) for x in (value and [value] or [])]
1167 elif field.type == 'integer':
1168 if value != None:
1169 fields[fieldname] = safe_int(value)
1170 elif field.type.startswith('reference'):
1171 if value != None and isinstance(self.table, Table) and not keyed:
1172 fields[fieldname] = safe_int(value)
1173 elif field.type == 'double':
1174 if value != None:
1175 fields[fieldname] = safe_float(value)
1176
1177 for fieldname in self.vars:
1178 if fieldname != 'id' and fieldname in self.table.fields\
1179 and not fieldname in fields and not fieldname\
1180 in request_vars:
1181 fields[fieldname] = self.vars[fieldname]
1182
1183 if dbio:
1184 if keyed:
1185 if reduce(lambda x, y: x and y, record_id.values()): # if record_id
1186 if fields:
1187 qry = reduce(lambda x, y: x & y,
1188 [self.table[k] == self.record[k] for k in self.table._primarykey])
1189 self.table._db(qry).update(**fields)
1190 else:
1191 pk = self.table.insert(**fields)
1192 if pk:
1193 self.vars.update(pk)
1194 else:
1195 ret = False
1196 else:
1197 if record_id:
1198 self.vars.id = self.record.id
1199 if fields:
1200 self.table._db(self.table.id == self.record.id).update(**fields)
1201 else:
1202 self.vars.id = self.table.insert(**fields)
1203 return ret
1204
1205 @staticmethod
1207 """
1208 generates a SQLFORM for the given fields.
1209
1210 Internally will build a non-database based data model
1211 to hold the fields.
1212 """
1213 # Define a table name, this way it can be logical to our CSS.
1214 # And if you switch from using SQLFORM to SQLFORM.factory
1215 # your same css definitions will still apply.
1216
1217 table_name = attributes.get('table_name', 'no_table')
1218
1219 # So it won't interfear with SQLDB.define_table
1220 if 'table_name' in attributes:
1221 del attributes['table_name']
1222
1223 return SQLFORM(DAL(None).define_table(table_name, *fields), **attributes)
1224
1225
1227
1228 """
1229 given a Rows object, as returned by a db().select(), generates
1230 an html table with the rows.
1231
1232 optional arguments:
1233
1234 :param linkto: URL (or lambda to generate a URL) to edit individual records
1235 :param upload: URL to download uploaded files
1236 :param orderby: Add an orderby link to column headers.
1237 :param headers: dictionary of headers to headers redefinions
1238 headers can also be a string to gerenare the headers from data
1239 for now only headers="fieldname:capitalize",
1240 headers="labels" and headers=None are supported
1241 :param truncate: length at which to truncate text in table cells.
1242 Defaults to 16 characters.
1243 :param columns: a list or dict contaning the names of the columns to be shown
1244 Defaults to all
1245
1246 Optional names attributes for passed to the <table> tag
1247
1248 The keys of headers and columns must be of the form "tablename.fieldname"
1249
1250 Simple linkto example::
1251
1252 rows = db.select(db.sometable.ALL)
1253 table = SQLTABLE(rows, linkto='someurl')
1254
1255 This will link rows[id] to .../sometable/value_of_id
1256
1257
1258 More advanced linkto example::
1259
1260 def mylink(field, type, ref):
1261 return URL(r=request, args=[field])
1262
1263 rows = db.select(db.sometable.ALL)
1264 table = SQLTABLE(rows, linkto=mylink)
1265
1266 This will link rows[id] to
1267 current_app/current_controlle/current_function/value_of_id
1268
1269
1270 """
1271
1272 - def __init__(
1273 self,
1274 sqlrows,
1275 linkto=None,
1276 upload=None,
1277 orderby=None,
1278 headers={},
1279 truncate=16,
1280 columns=None,
1281 th_link='',
1282 **attributes
1283 ):
1284
1285 TABLE.__init__(self, **attributes)
1286 self.components = []
1287 self.attributes = attributes
1288 self.sqlrows = sqlrows
1289 (components, row) = (self.components, [])
1290 if not columns:
1291 columns = sqlrows.colnames
1292 if headers=='fieldname:capitalize':
1293 headers = {}
1294 for c in columns:
1295 headers[c] = ' '.join([w.capitalize() for w in c.split('.')[-1].split('_')])
1296 elif headers=='labels':
1297 headers = {}
1298 for c in columns:
1299 (t,f) = c.split('.')
1300 field = sqlrows.db[t][f]
1301 headers[c] = field.label
1302
1303 if headers!=None:
1304 for c in columns:
1305 if orderby:
1306 row.append(TH(A(headers.get(c, c),
1307 _href=th_link+'?orderby=' + c)))
1308 else:
1309 row.append(TH(headers.get(c, c)))
1310 components.append(THEAD(TR(*row)))
1311
1312 tbody = []
1313 for (rc, record) in enumerate(sqlrows):
1314 row = []
1315 if rc % 2 == 0:
1316 _class = 'even'
1317 else:
1318 _class = 'odd'
1319 for colname in columns:
1320 if not table_field.match(colname):
1321 if "_extra" in record and colname in record._extra:
1322 r = record._extra[colname]
1323 row.append(TD(r))
1324 continue
1325 else:
1326 raise KeyError("Column %s not found (SQLTABLE)" % colname)
1327 (tablename, fieldname) = colname.split('.')
1328 try:
1329 field = sqlrows.db[tablename][fieldname]
1330 except KeyError:
1331 field = None
1332 if tablename in record \
1333 and isinstance(record,Row) \
1334 and isinstance(record[tablename],Row):
1335 r = record[tablename][fieldname]
1336 elif fieldname in record:
1337 r = record[fieldname]
1338 else:
1339 raise SyntaxError, 'something wrong in Rows object'
1340 r_old = r
1341 if not field:
1342 pass
1343 elif linkto and field.type == 'id':
1344 try:
1345 href = linkto(r, 'table', tablename)
1346 except TypeError:
1347 href = '%s/%s/%s' % (linkto, tablename, r_old)
1348 r = A(r, _href=href)
1349 elif field.type.startswith('reference'):
1350 if linkto:
1351 ref = field.type[10:]
1352 try:
1353 href = linkto(r, 'reference', ref)
1354 except TypeError:
1355 href = '%s/%s/%s' % (linkto, ref, r_old)
1356 if ref.find('.') >= 0:
1357 tref,fref = ref.split('.')
1358 if hasattr(sqlrows.db[tref],'_primarykey'):
1359 href = '%s/%s?%s' % (linkto, tref, urllib.urlencode({fref:r}))
1360 if field.represent:
1361 r = A(field.represent(r), _href=str(href))
1362 else:
1363 r = A(str(r), _href=str(href))
1364 elif field.represent:
1365 r = field.represent(r)
1366 elif linkto and hasattr(field._table,'_primarykey') and fieldname in field._table._primarykey:
1367 # have to test this with multi-key tables
1368 key = urllib.urlencode(dict( [ \
1369 ((tablename in record \
1370 and isinstance(record, Row) \
1371 and isinstance(record[tablename], Row)) and
1372 (k, record[tablename][k])) or (k, record[k]) \
1373 for k in field._table._primarykey ] ))
1374 r = A(r, _href='%s/%s?%s' % (linkto, tablename, key))
1375 elif field.type.startswith('list:'):
1376 r = field.represent(r or [])
1377 elif field.represent:
1378 r = field.represent(r)
1379 elif field.type == 'blob' and r:
1380 r = 'DATA'
1381 elif field.type == 'upload':
1382 if upload and r:
1383 r = A('file', _href='%s/%s' % (upload, r))
1384 elif r:
1385 r = 'file'
1386 else:
1387 r = ''
1388 elif field.type in ['string','text']:
1389 r = str(field.formatter(r))
1390 ur = unicode(r, 'utf8')
1391 if truncate!=None and len(ur) > truncate:
1392 r = ur[:truncate - 3].encode('utf8') + '...'
1393 row.append(TD(r))
1394 tbody.append(TR(_class=_class, *row))
1395 components.append(TBODY(*tbody))
1396
1397 form_factory = SQLFORM.factory # for backward compatibility, deprecated
1398
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0beta1 on Mon Apr 25 15:04:09 2011 | http://epydoc.sourceforge.net |