| 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 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, 'formstyle 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 isinstance(field.type,str) and 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 'delete_this_record' in fields:
1185 # this should never happen but seems to happen to some
1186 del fields['delete_this_record']
1187 if keyed:
1188 if reduce(lambda x, y: x and y, record_id.values()): # if record_id
1189 if fields:
1190 qry = reduce(lambda x, y: x & y,
1191 [self.table[k] == self.record[k] for k in self.table._primarykey])
1192 self.table._db(qry).update(**fields)
1193 else:
1194 pk = self.table.insert(**fields)
1195 if pk:
1196 self.vars.update(pk)
1197 else:
1198 ret = False
1199 else:
1200 if record_id:
1201 self.vars.id = self.record.id
1202 if fields:
1203 self.table._db(self.table._id == self.record.id).update(**fields)
1204 else:
1205 self.vars.id = self.table.insert(**fields)
1206 return ret
1207
1208 @staticmethod
1210 """
1211 generates a SQLFORM for the given fields.
1212
1213 Internally will build a non-database based data model
1214 to hold the fields.
1215 """
1216 # Define a table name, this way it can be logical to our CSS.
1217 # And if you switch from using SQLFORM to SQLFORM.factory
1218 # your same css definitions will still apply.
1219
1220 table_name = attributes.get('table_name', 'no_table')
1221
1222 # So it won't interfear with SQLDB.define_table
1223 if 'table_name' in attributes:
1224 del attributes['table_name']
1225
1226 return SQLFORM(DAL(None).define_table(table_name, *fields), **attributes)
1227
1228
1230
1231 """
1232 given a Rows object, as returned by a db().select(), generates
1233 an html table with the rows.
1234
1235 optional arguments:
1236
1237 :param linkto: URL (or lambda to generate a URL) to edit individual records
1238 :param upload: URL to download uploaded files
1239 :param orderby: Add an orderby link to column headers.
1240 :param headers: dictionary of headers to headers redefinions
1241 headers can also be a string to gerenare the headers from data
1242 for now only headers="fieldname:capitalize",
1243 headers="labels" and headers=None are supported
1244 :param truncate: length at which to truncate text in table cells.
1245 Defaults to 16 characters.
1246 :param columns: a list or dict contaning the names of the columns to be shown
1247 Defaults to all
1248
1249 Optional names attributes for passed to the <table> tag
1250
1251 The keys of headers and columns must be of the form "tablename.fieldname"
1252
1253 Simple linkto example::
1254
1255 rows = db.select(db.sometable.ALL)
1256 table = SQLTABLE(rows, linkto='someurl')
1257
1258 This will link rows[id] to .../sometable/value_of_id
1259
1260
1261 More advanced linkto example::
1262
1263 def mylink(field, type, ref):
1264 return URL(r=request, args=[field])
1265
1266 rows = db.select(db.sometable.ALL)
1267 table = SQLTABLE(rows, linkto=mylink)
1268
1269 This will link rows[id] to
1270 current_app/current_controlle/current_function/value_of_id
1271
1272
1273 """
1274
1275 - def __init__(
1276 self,
1277 sqlrows,
1278 linkto=None,
1279 upload=None,
1280 orderby=None,
1281 headers={},
1282 truncate=16,
1283 columns=None,
1284 th_link='',
1285 **attributes
1286 ):
1287
1288 TABLE.__init__(self, **attributes)
1289 self.components = []
1290 self.attributes = attributes
1291 self.sqlrows = sqlrows
1292 (components, row) = (self.components, [])
1293 if not sqlrows:
1294 return
1295 if not columns:
1296 columns = sqlrows.colnames
1297 if headers=='fieldname:capitalize':
1298 headers = {}
1299 for c in columns:
1300 headers[c] = ' '.join([w.capitalize() for w in c.split('.')[-1].split('_')])
1301 elif headers=='labels':
1302 headers = {}
1303 for c in columns:
1304 (t,f) = c.split('.')
1305 field = sqlrows.db[t][f]
1306 headers[c] = field.label
1307
1308 if headers!=None:
1309 for c in columns:
1310 if orderby:
1311 row.append(TH(A(headers.get(c, c),
1312 _href=th_link+'?orderby=' + c)))
1313 else:
1314 row.append(TH(headers.get(c, c)))
1315 components.append(THEAD(TR(*row)))
1316
1317 tbody = []
1318 for (rc, record) in enumerate(sqlrows):
1319 row = []
1320 if rc % 2 == 0:
1321 _class = 'even'
1322 else:
1323 _class = 'odd'
1324 for colname in columns:
1325 if not table_field.match(colname):
1326 if "_extra" in record and colname in record._extra:
1327 r = record._extra[colname]
1328 row.append(TD(r))
1329 continue
1330 else:
1331 raise KeyError("Column %s not found (SQLTABLE)" % colname)
1332 (tablename, fieldname) = colname.split('.')
1333 try:
1334 field = sqlrows.db[tablename][fieldname]
1335 except KeyError:
1336 field = None
1337 if tablename in record \
1338 and isinstance(record,Row) \
1339 and isinstance(record[tablename],Row):
1340 r = record[tablename][fieldname]
1341 elif fieldname in record:
1342 r = record[fieldname]
1343 else:
1344 raise SyntaxError, 'something wrong in Rows object'
1345 r_old = r
1346 if not field:
1347 pass
1348 elif linkto and field.type == 'id':
1349 try:
1350 href = linkto(r, 'table', tablename)
1351 except TypeError:
1352 href = '%s/%s/%s' % (linkto, tablename, r_old)
1353 r = A(r, _href=href)
1354 elif field.type.startswith('reference'):
1355 if linkto:
1356 ref = field.type[10:]
1357 try:
1358 href = linkto(r, 'reference', ref)
1359 except TypeError:
1360 href = '%s/%s/%s' % (linkto, ref, r_old)
1361 if ref.find('.') >= 0:
1362 tref,fref = ref.split('.')
1363 if hasattr(sqlrows.db[tref],'_primarykey'):
1364 href = '%s/%s?%s' % (linkto, tref, urllib.urlencode({fref:r}))
1365 if field.represent:
1366 r = A(field.represent(r), _href=str(href))
1367 else:
1368 r = A(str(r), _href=str(href))
1369 elif field.represent:
1370 r = field.represent(r)
1371 elif linkto and hasattr(field._table,'_primarykey') and fieldname in field._table._primarykey:
1372 # have to test this with multi-key tables
1373 key = urllib.urlencode(dict( [ \
1374 ((tablename in record \
1375 and isinstance(record, Row) \
1376 and isinstance(record[tablename], Row)) and
1377 (k, record[tablename][k])) or (k, record[k]) \
1378 for k in field._table._primarykey ] ))
1379 r = A(r, _href='%s/%s?%s' % (linkto, tablename, key))
1380 elif field.type.startswith('list:'):
1381 r = field.represent(r or [])
1382 elif field.represent:
1383 r = field.represent(r)
1384 elif field.type == 'blob' and r:
1385 r = 'DATA'
1386 elif field.type == 'upload':
1387 if upload and r:
1388 r = A('file', _href='%s/%s' % (upload, r))
1389 elif r:
1390 r = 'file'
1391 else:
1392 r = ''
1393 elif field.type in ['string','text']:
1394 r = str(field.formatter(r))
1395 ur = unicode(r, 'utf8')
1396 if truncate!=None and len(ur) > truncate:
1397 r = ur[:truncate - 3].encode('utf8') + '...'
1398 row.append(TD(r))
1399 tbody.append(TR(_class=_class, *row))
1400 components.append(TBODY(*tbody))
1401
1402 form_factory = SQLFORM.factory # for backward compatibility, deprecated
1403
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0beta1 on Tue Jun 7 21:08:10 2011 | http://epydoc.sourceforge.net |