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