| 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 try:
17 from urlparse import parse_qs as psq
18 except ImportError:
19 from cgi import parse_qs as psq
20 import os
21 from http import HTTP
22 from html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT
23 from html import FORM, INPUT, LABEL, OPTION, SELECT, BUTTON
24 from html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE
25 from html import URL, truncate_string, FIELDSET
26 from dal import DAL, Field, Table, Row, CALLABLETYPES, smart_query, \
27 bar_encode, Reference, REGEX_TABLE_DOT_FIELD
28 from storage import Storage
29 from utils import md5_hash
30 from validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \
31 IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, IS_STRONG
32
33 import datetime
34 import urllib
35 import re
36 import cStringIO
37 from gluon import current, redirect, A, URL, DIV, H3, UL, LI, SPAN, INPUT
38 import inspect
39 import settings
40 is_gae = settings.global_settings.web2py_runtime_gae
41
42 table_field = re.compile('[\w_]+\.[\w_]+')
43 widget_class = re.compile('^\w*')
44
45
48
49
51 f = field.represent
52 if not callable(f):
53 return str(value)
54 n = f.func_code.co_argcount - len(f.func_defaults or [])
55 if getattr(f, 'im_self', None):
56 n -= 1
57 if n == 1:
58 return f(value)
59 elif n == 2:
60 return f(value, record)
61 else:
62 raise RuntimeError("field representation must take 1 or 2 args")
63
64
70
71
77
78
80 """
81 helper for SQLFORM to generate form input fields
82 (widget), related to the fieldtype
83 """
84
85 _class = 'generic-widget'
86
87 @classmethod
90 """
91 helper to build a common set of attributes
92
93 :param field: the field involved,
94 some attributes are derived from this
95 :param widget_attributes: widget related attributes
96 :param attributes: any other supplied attributes
97 """
98 attr = dict(
99 _id='%s_%s' % (field._tablename, field.name),
100 _class=cls._class or
101 widget_class.match(str(field.type)).group(),
102 _name=field.name,
103 requires=field.requires,
104 )
105 attr.update(widget_attributes)
106 attr.update(attributes)
107 return attr
108
109 @classmethod
111 """
112 generates the widget for the field.
113
114 When serialized, will provide an INPUT tag:
115
116 - id = tablename_fieldname
117 - class = field.type
118 - name = fieldname
119
120 :param field: the field needing the widget
121 :param value: value
122 :param attributes: any other attributes to be applied
123 """
124
125 raise NotImplementedError
126
127
129 _class = 'string'
130
131 @classmethod
133 """
134 generates an INPUT text tag.
135
136 see also: :meth:`FormWidget.widget`
137 """
138
139 default = dict(
140 _type='text',
141 value=(not value is None and str(value)) or '',
142 )
143 attr = cls._attributes(field, default, **attributes)
144
145 return INPUT(**attr)
146
147
149 _class = 'integer'
150
151
153 _class = 'double'
154
155
157 _class = 'decimal'
158
159
161 _class = 'time'
162
163
165 _class = 'date'
166
167
169 _class = 'datetime'
170
171
173 _class = 'text'
174
175 @classmethod
177 """
178 generates a TEXTAREA tag.
179
180 see also: :meth:`FormWidget.widget`
181 """
182
183 default = dict(value=value)
184 attr = cls._attributes(field, default, **attributes)
185 return TEXTAREA(**attr)
186
187
189 _class = 'boolean'
190
191 @classmethod
193 """
194 generates an INPUT checkbox tag.
195
196 see also: :meth:`FormWidget.widget`
197 """
198
199 default = dict(_type='checkbox', value=value)
200 attr = cls._attributes(field, default,
201 **attributes)
202 return INPUT(**attr)
203
204
206
207 @staticmethod
209 """
210 checks if the field has selectable options
211
212 :param field: the field needing checking
213 :returns: True if the field has options
214 """
215
216 return hasattr(field.requires, 'options')
217
218 @classmethod
220 """
221 generates a SELECT tag, including OPTIONs (only 1 option allowed)
222
223 see also: :meth:`FormWidget.widget`
224 """
225 default = dict(value=value)
226 attr = cls._attributes(field, default,
227 **attributes)
228 requires = field.requires
229 if not isinstance(requires, (list, tuple)):
230 requires = [requires]
231 if requires:
232 if hasattr(requires[0], 'options'):
233 options = requires[0].options()
234 else:
235 raise SyntaxError(
236 'widget cannot determine options of %s' % field)
237 opts = [OPTION(v, _value=k) for (k, v) in options]
238
239 return SELECT(*opts, **attr)
240
241
243
244 @classmethod
246 _id = '%s_%s' % (field._tablename, field.name)
247 _name = field.name
248 if field.type == 'list:integer':
249 _class = 'integer'
250 else:
251 _class = 'string'
252 requires = field.requires if isinstance(
253 field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None
254 attributes['_style'] = 'list-style:none'
255 nvalue = value or ['']
256 items = [LI(INPUT(_id=_id, _class=_class, _name=_name,
257 value=v, hideerror=k < len(nvalue) - 1,
258 requires=requires),
259 **attributes) for (k, v) in enumerate(nvalue)]
260 script = SCRIPT("""
261 // from http://refactormycode.com/codes/694-expanding-input-list-using-jquery
262 (function(){
263 jQuery.fn.grow_input = function() {
264 return this.each(function() {
265 var ul = this;
266 jQuery(ul).find(":text").after('<a href="javascript:void(0)">+</a> <a href="javascript:void(0)">-</a>').keypress(function (e) { return (e.which == 13) ? pe(ul, e) : true; }).next().click(function(e){ pe(ul, e) }).next().click(function(e){ rl(ul, e)});
267 });
268 };
269 function pe(ul, e) {
270 var new_line = ml(ul);
271 rel(ul);
272 new_line.insertAfter(jQuery(e.target).parent());
273 new_line.find(":text").focus();
274 return false;
275 }
276 function rl(ul, e) {
277 jQuery(e.target).parent().remove();
278 }
279 function ml(ul) {
280 var line = jQuery(ul).find("li:first").clone(true);
281 line.find(':text').val('');
282 return line;
283 }
284 function rel(ul) {
285 jQuery(ul).find("li").each(function() {
286 var trimmed = jQuery.trim(jQuery(this.firstChild).val());
287 if (trimmed=='') jQuery(this).remove(); else jQuery(this.firstChild).val(trimmed);
288 });
289 }
290 })();
291 jQuery(document).ready(function(){jQuery('#%s_grow_input').grow_input();});
292 """ % _id)
293 attributes['_id'] = _id + '_grow_input'
294 attributes['_style'] = 'list-style:none'
295 return TAG[''](UL(*items, **attributes), script)
296
297
299
300 @classmethod
302 """
303 generates a SELECT tag, including OPTIONs (multiple options allowed)
304
305 see also: :meth:`FormWidget.widget`
306
307 :param size: optional param (default=5) to indicate how many rows must
308 be shown
309 """
310
311 attributes.update(_size=size, _multiple=True)
312
313 return OptionsWidget.widget(field, value, **attributes)
314
315
317
318 @classmethod
320 """
321 generates a TABLE tag, including INPUT radios (only 1 option allowed)
322
323 see also: :meth:`FormWidget.widget`
324 """
325
326 attr = cls._attributes(field, {}, **attributes)
327 attr['_class'] = attr.get('_class', 'web2py_radiowidget')
328
329 requires = field.requires
330 if not isinstance(requires, (list, tuple)):
331 requires = [requires]
332 if requires:
333 if hasattr(requires[0], 'options'):
334 options = requires[0].options()
335 else:
336 raise SyntaxError('widget cannot determine options of %s'
337 % field)
338 options = [(k, v) for k, v in options if str(v)]
339 opts = []
340 cols = attributes.get('cols', 1)
341 totals = len(options)
342 mods = totals % cols
343 rows = totals / cols
344 if mods:
345 rows += 1
346
347 #widget style
348 wrappers = dict(
349 table=(TABLE, TR, TD),
350 ul=(DIV, UL, LI),
351 divs=(CAT, DIV, DIV)
352 )
353 parent, child, inner = wrappers[attributes.get('style', 'table')]
354
355 for r_index in range(rows):
356 tds = []
357 for k, v in options[r_index * cols:(r_index + 1) * cols]:
358 checked = {'_checked': 'checked'} if k == value else {}
359 tds.append(inner(INPUT(_type='radio',
360 _id='%s%s' % (field.name, k),
361 _name=field.name,
362 requires=attr.get('requires', None),
363 hideerror=True, _value=k,
364 value=value,
365 **checked),
366 LABEL(v, _for='%s%s' % (field.name, k))))
367 opts.append(child(tds))
368
369 if opts:
370 opts[-1][0][0]['hideerror'] = False
371 return parent(*opts, **attr)
372
373
375
376 @classmethod
378 """
379 generates a TABLE tag, including INPUT checkboxes (multiple allowed)
380
381 see also: :meth:`FormWidget.widget`
382 """
383
384 # was values = re.compile('[\w\-:]+').findall(str(value))
385 if isinstance(value, (list, tuple)):
386 values = [str(v) for v in value]
387 else:
388 values = [str(value)]
389
390 attr = cls._attributes(field, {}, **attributes)
391 attr['_class'] = attr.get('_class', 'web2py_checkboxeswidget')
392
393 requires = field.requires
394 if not isinstance(requires, (list, tuple)):
395 requires = [requires]
396 if requires and hasattr(requires[0], 'options'):
397 options = requires[0].options()
398 else:
399 raise SyntaxError('widget cannot determine options of %s'
400 % field)
401
402 options = [(k, v) for k, v in options if k != '']
403 opts = []
404 cols = attributes.get('cols', 1)
405 totals = len(options)
406 mods = totals % cols
407 rows = totals / cols
408 if mods:
409 rows += 1
410
411 #widget style
412 wrappers = dict(
413 table=(TABLE, TR, TD),
414 ul=(DIV, UL, LI),
415 divs=(CAT, DIV, DIV)
416 )
417 parent, child, inner = wrappers[attributes.get('style', 'table')]
418
419 for r_index in range(rows):
420 tds = []
421 for k, v in options[r_index * cols:(r_index + 1) * cols]:
422 if k in values:
423 r_value = k
424 else:
425 r_value = []
426 tds.append(inner(INPUT(_type='checkbox',
427 _id='%s%s' % (field.name, k),
428 _name=field.name,
429 requires=attr.get('requires', None),
430 hideerror=True, _value=k,
431 value=r_value),
432 LABEL(v, _for='%s%s' % (field.name, k))))
433 opts.append(child(tds))
434
435 if opts:
436 opts.append(
437 INPUT(_class="hidden", requires=attr.get('requires', None),
438 _disabled="disabled", _name=field.name,
439 hideerror=False))
440 return parent(*opts, **attr)
441
442
444 _class = 'password'
445
446 DEFAULT_PASSWORD_DISPLAY = 8 * ('*')
447
448 @classmethod
450 """
451 generates a INPUT password tag.
452 If a value is present it will be shown as a number of '*', not related
453 to the length of the actual value.
454
455 see also: :meth:`FormWidget.widget`
456 """
457 # detect if attached a IS_STRONG with entropy
458 default = dict(
459 _type='password',
460 _value=(value and cls.DEFAULT_PASSWORD_DISPLAY) or '',
461 )
462 attr = cls._attributes(field, default, **attributes)
463 output = CAT(INPUT(**attr))
464
465 # deal with entropy check!
466 requires = field.requires
467 if not isinstance(requires, (list, tuple)):
468 requires = [requires]
469 is_strong = [r for r in requires if isinstance(r, IS_STRONG)]
470 if is_strong:
471 output.append(SCRIPT("web2py_validate_entropy(jQuery('#%s'),%s);" % (
472 attr['_id'], is_strong[0].entropy
473 if is_strong[0].entropy else "null")))
474 # end entropy check
475 return output
476
477
479 _class = 'upload'
480
481 DEFAULT_WIDTH = '150px'
482 ID_DELETE_SUFFIX = '__delete'
483 GENERIC_DESCRIPTION = 'file'
484 DELETE_FILE = 'delete'
485
486 @classmethod
488 """
489 generates a INPUT file tag.
490
491 Optionally provides an A link to the file, including a checkbox so
492 the file can be deleted.
493 All is wrapped in a DIV.
494
495 see also: :meth:`FormWidget.widget`
496
497 :param download_url: Optional URL to link to the file (default = None)
498 """
499
500 default = dict(_type='file',)
501 attr = cls._attributes(field, default, **attributes)
502
503 inp = INPUT(**attr)
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 (br, image) = ('', '')
511 if UploadWidget.is_image(value):
512 br = BR()
513 image = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
514
515 requires = attr["requires"]
516 if requires == [] or isinstance(requires, IS_EMPTY_OR):
517 inp = DIV(inp, '[',
518 A(current.T(
519 UploadWidget.GENERIC_DESCRIPTION), _href=url),
520 '|',
521 INPUT(_type='checkbox',
522 _name=field.name + cls.ID_DELETE_SUFFIX,
523 _id=field.name + cls.ID_DELETE_SUFFIX),
524 LABEL(current.T(cls.DELETE_FILE),
525 _for=field.name + cls.ID_DELETE_SUFFIX),
526 ']', br, image)
527 else:
528 inp = DIV(inp, '[',
529 A(cls.GENERIC_DESCRIPTION, _href=url),
530 ']', br, image)
531 return inp
532
533 @classmethod
535 """
536 how to represent the file:
537
538 - with download url and if it is an image: <A href=...><IMG ...></A>
539 - otherwise with download url: <A href=...>file</A>
540 - otherwise: file
541
542 :param field: the field
543 :param value: the field value
544 :param download_url: url for the file download (default = None)
545 """
546
547 inp = cls.GENERIC_DESCRIPTION
548
549 if download_url and value:
550 if callable(download_url):
551 url = download_url(value)
552 else:
553 url = download_url + '/' + value
554 if cls.is_image(value):
555 inp = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
556 inp = A(inp, _href=url)
557
558 return inp
559
560 @staticmethod
562 """
563 Tries to check if the filename provided references to an image
564
565 Checking is based on filename extension. Currently recognized:
566 gif, png, jp(e)g, bmp
567
568 :param value: filename
569 """
570
571 extension = value.split('.')[-1].lower()
572 if extension in ['gif', 'png', 'jpg', 'jpeg', 'bmp']:
573 return True
574 return False
575
576
578 _class = 'string'
579
580 - def __init__(self, request, field, id_field=None, db=None,
581 orderby=None, limitby=(0, 10), distinct=False,
582 keyword='_autocomplete_%(tablename)s_%(fieldname)s',
583 min_length=2, help_fields=None, help_string=None):
584
585 self.help_fields = help_fields or []
586 self.help_string = help_string
587 if self.help_fields and not self.help_string:
588 self.help_string = ' '.join('%%(%s)s' for f in self.help_fields)
589
590 self.request = request
591 self.keyword = keyword % dict(tablename=field.tablename,
592 fieldname=field.name)
593 self.db = db or field._db
594 self.orderby = orderby
595 self.limitby = limitby
596 self.distinct = distinct
597 self.min_length = min_length
598 self.fields = [field]
599 if id_field:
600 self.is_reference = True
601 self.fields.append(id_field)
602 else:
603 self.is_reference = False
604 if hasattr(request, 'application'):
605 self.url = URL(args=request.args)
606 self.callback()
607 else:
608 self.url = request
609
611 if self.keyword in self.request.vars:
612 field = self.fields[0]
613 if is_gae:
614 rows = self.db(field.__ge__(self.request.vars[self.keyword]) & field.__lt__(self.request.vars[self.keyword] + u'\ufffd')).select(orderby=self.orderby, limitby=self.limitby, *self.fields)
615 else:
616 rows = self.db(field.like(self.request.vars[self.keyword] + '%')).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *self.fields)
617 if rows:
618 if self.is_reference:
619 id_field = self.fields[1]
620 if self.help_fields:
621 options = [OPTION(
622 self.help_string % dict([(h.name, s[h.name]) for h in self.fields[:1] + self.help_fields]),
623 _value=s[id_field.name], _selected=(k == 0)) for k, s in enumerate(rows)]
624 else:
625 options = [OPTION(
626 s[field.name], _value=s[id_field.name],
627 _selected=(k == 0)) for k, s in enumerate(rows)]
628 raise HTTP(
629 200, SELECT(_id=self.keyword, _class='autocomplete',
630 _size=len(rows), _multiple=(len(rows) == 1),
631 *options).xml())
632 else:
633 raise HTTP(
634 200, SELECT(_id=self.keyword, _class='autocomplete',
635 _size=len(rows), _multiple=(len(rows) == 1),
636 *[OPTION(s[field.name],
637 _selected=(k == 0))
638 for k, s in enumerate(rows)]).xml())
639 else:
640 raise HTTP(200, '')
641
643 default = dict(
644 _type='text',
645 value=(not value is None and str(value)) or '',
646 )
647 attr = StringWidget._attributes(field, default, **attributes)
648 div_id = self.keyword + '_div'
649 attr['_autocomplete'] = 'off'
650 if self.is_reference:
651 key2 = self.keyword + '_aux'
652 key3 = self.keyword + '_auto'
653 attr['_class'] = 'string'
654 name = attr['_name']
655 if 'requires' in attr:
656 del attr['requires']
657 attr['_name'] = key2
658 value = attr['value']
659 record = self.db(
660 self.fields[1] == value).select(self.fields[0]).first()
661 attr['value'] = record and record[self.fields[0].name]
662 attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
663 dict(div_id=div_id, u='F' + self.keyword)
664 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='+encodeURIComponent(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');" % \
665 dict(url=self.url, min_length=self.min_length,
666 key=self.keyword, id=attr['_id'], key2=key2, key3=key3,
667 name=name, div_id=div_id, u='F' + self.keyword)
668 if self.min_length == 0:
669 attr['_onfocus'] = attr['_onkeyup']
670 return TAG[''](INPUT(**attr), INPUT(_type='hidden', _id=key3, _value=value,
671 _name=name, requires=field.requires),
672 DIV(_id=div_id, _style='position:absolute;'))
673 else:
674 attr['_name'] = field.name
675 attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
676 dict(div_id=div_id, u='F' + self.keyword)
677 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='+encodeURIComponent(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');" % \
678 dict(url=self.url, min_length=self.min_length,
679 key=self.keyword, id=attr['_id'], div_id=div_id, u='F' + self.keyword)
680 if self.min_length == 0:
681 attr['_onfocus'] = attr['_onkeyup']
682 return TAG[''](INPUT(**attr), DIV(_id=div_id, _style='position:absolute;'))
683
684
686 ''' 3 column table - default '''
687 table = TABLE()
688 for id, label, controls, help in fields:
689 _help = TD(help, _class='w2p_fc')
690 _controls = TD(controls, _class='w2p_fw')
691 _label = TD(label, _class='w2p_fl')
692 table.append(TR(_label, _controls, _help, _id=id))
693 return table
694
695
697 ''' 2 column table '''
698 table = TABLE()
699 for id, label, controls, help in fields:
700 _help = TD(help, _class='w2p_fc', _width='50%')
701 _controls = TD(controls, _class='w2p_fw', _colspan='2')
702 _label = TD(label, _class='w2p_fl', _width='50%')
703 table.append(TR(_label, _help, _id=id + '1', _class='even'))
704 table.append(TR(_controls, _id=id + '2', _class='odd'))
705 return table
706
707
709 ''' divs only '''
710 table = TAG['']()
711 for id, label, controls, help in fields:
712 _help = DIV(help, _class='w2p_fc')
713 _controls = DIV(controls, _class='w2p_fw')
714 _label = DIV(label, _class='w2p_fl')
715 table.append(DIV(_label, _controls, _help, _id=id))
716 return table
717
718
720 ''' divs only '''
721 if len(fields) != 2:
722 raise RuntimeError("Not possible")
723 id, label, controls, help = fields[0]
724 submit_button = fields[1][2]
725 return CAT(DIV(controls, _style='display:inline'),
726 submit_button)
727
728
730 ''' unordered list '''
731 table = UL()
732 for id, label, controls, help in fields:
733 _help = DIV(help, _class='w2p_fc')
734 _controls = DIV(controls, _class='w2p_fw')
735 _label = DIV(label, _class='w2p_fl')
736 table.append(LI(_label, _controls, _help, _id=id))
737 return table
738
739
741 ''' bootstrap format form layout '''
742 form['_class'] = 'form-horizontal'
743 parent = FIELDSET()
744 for id, label, controls, help in fields:
745 # wrappers
746 _help = SPAN(help, _class='help-inline')
747 # embed _help into _controls
748 _controls = DIV(controls, _help, _class='controls')
749 # submit unflag by default
750 _submit = False
751
752 if isinstance(controls, INPUT):
753 controls.add_class('input-xlarge')
754 if controls['_type'] == 'submit':
755 # flag submit button
756 _submit = True
757 controls['_class'] = 'btn btn-primary'
758 if controls['_type'] == 'file':
759 controls['_class'] = 'input-file'
760
761 # For password fields, which are wrapped in a CAT object.
762 if isinstance(controls, CAT) and isinstance(controls[0], INPUT):
763 controls[0].add_class('input-xlarge')
764
765 if isinstance(controls, SELECT):
766 controls.add_class('input-xlarge')
767
768 if isinstance(controls, TEXTAREA):
769 controls.add_class('input-xlarge')
770
771 if isinstance(label, LABEL):
772 label['_class'] = 'control-label'
773
774 if _submit:
775 # submit button has unwrapped label and controls, different class
776 parent.append(DIV(label, controls, _class='form-actions'))
777 # unflag submit (possible side effect)
778 _submit = False
779 else:
780 # unwrapped label
781 parent.append(DIV(label, _controls, _class='control-group'))
782 return parent
783
784
786
787 """
788 SQLFORM is used to map a table (and a current record) into an HTML form
789
790 given a SQLTable stored in db.table
791
792 generates an insert form::
793
794 SQLFORM(db.table)
795
796 generates an update form::
797
798 record=db.table[some_id]
799 SQLFORM(db.table, record)
800
801 generates an update with a delete button::
802
803 SQLFORM(db.table, record, deletable=True)
804
805 if record is an int::
806
807 record=db.table[record]
808
809 optional arguments:
810
811 :param fields: a list of fields that should be placed in the form,
812 default is all.
813 :param labels: a dictionary with labels for each field, keys are the field
814 names.
815 :param col3: a dictionary with content for an optional third column
816 (right of each field). keys are field names.
817 :param linkto: the URL of a controller/function to access referencedby
818 records
819 see controller appadmin.py for examples
820 :param upload: the URL of a controller/function to download an uploaded file
821 see controller appadmin.py for examples
822
823 any named optional attribute is passed to the <form> tag
824 for example _class, _id, _style, _action, _method, etc.
825
826 """
827
828 # usability improvements proposal by fpp - 4 May 2008 :
829 # - correct labels (for points to field id, not field name)
830 # - add label for delete checkbox
831 # - add translatable label for record ID
832 # - add third column to right of fields, populated from the col3 dict
833
834 widgets = Storage(dict(
835 string=StringWidget,
836 text=TextWidget,
837 password=PasswordWidget,
838 integer=IntegerWidget,
839 double=DoubleWidget,
840 decimal=DecimalWidget,
841 time=TimeWidget,
842 date=DateWidget,
843 datetime=DatetimeWidget,
844 upload=UploadWidget,
845 boolean=BooleanWidget,
846 blob=None,
847 options=OptionsWidget,
848 multiple=MultipleOptionsWidget,
849 radio=RadioWidget,
850 checkboxes=CheckboxesWidget,
851 autocomplete=AutocompleteWidget,
852 list=ListWidget,
853 ))
854
855 formstyles = Storage(dict(
856 table3cols=formstyle_table3cols,
857 table2cols=formstyle_table2cols,
858 divs=formstyle_divs,
859 ul=formstyle_ul,
860 bootstrap=formstyle_bootstrap,
861 inline=formstyle_inline,
862 ))
863
864 FIELDNAME_REQUEST_DELETE = 'delete_this_record'
865 FIELDKEY_DELETE_RECORD = 'delete_record'
866 ID_LABEL_SUFFIX = '__label'
867 ID_ROW_SUFFIX = '__row'
868
870 if not status and self.record and self.errors:
871 ### if there are errors in update mode
872 # and some errors refers to an already uploaded file
873 # delete error if
874 # - user not trying to upload a new file
875 # - there is existing file and user is not trying to delete it
876 # this is because removing the file may not pass validation
877 for key in self.errors.keys():
878 if key in self.table \
879 and self.table[key].type == 'upload' \
880 and request_vars.get(key, None) in (None, '') \
881 and self.record[key] \
882 and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars:
883 del self.errors[key]
884 if not self.errors:
885 status = True
886 return status
887
888 - def __init__(
889 self,
890 table,
891 record=None,
892 deletable=False,
893 linkto=None,
894 upload=None,
895 fields=None,
896 labels=None,
897 col3={},
898 submit_button='Submit',
899 delete_label='Check to delete',
900 showid=True,
901 readonly=False,
902 comments=True,
903 keepopts=[],
904 ignore_rw=False,
905 record_id=None,
906 formstyle='table3cols',
907 buttons=['submit'],
908 separator=': ',
909 **attributes
910 ):
911 """
912 SQLFORM(db.table,
913 record=None,
914 fields=['name'],
915 labels={'name': 'Your name'},
916 linkto=URL(f='table/db/')
917 """
918 T = current.T
919
920 self.ignore_rw = ignore_rw
921 self.formstyle = formstyle
922 self.readonly = readonly
923 # Default dbio setting
924 self.detect_record_change = None
925
926 nbsp = XML(' ') # Firefox2 does not display fields with blanks
927 FORM.__init__(self, *[], **attributes)
928 ofields = fields
929 keyed = hasattr(table, '_primarykey') # for backward compatibility
930
931 # if no fields are provided, build it from the provided table
932 # will only use writable or readable fields, unless forced to ignore
933 if fields is None:
934 fields = [f.name for f in table if
935 (ignore_rw or f.writable or f.readable) and
936 (readonly or not f.compute)]
937 self.fields = fields
938
939 # make sure we have an id
940 if self.fields[0] != table.fields[0] and \
941 isinstance(table, Table) and not keyed:
942 self.fields.insert(0, table.fields[0])
943
944 self.table = table
945
946 # try to retrieve the indicated record using its id
947 # otherwise ignore it
948 if record and isinstance(record, (int, long, str, unicode)):
949 if not str(record).isdigit():
950 raise HTTP(404, "Object not found")
951 record = table._db(table._id == record).select().first()
952 if not record:
953 raise HTTP(404, "Object not found")
954 self.record = record
955
956 self.record_id = record_id
957 if keyed:
958 self.record_id = dict([(k, record and str(record[k]) or None)
959 for k in table._primarykey])
960 self.field_parent = {}
961 xfields = []
962 self.fields = fields
963 self.custom = Storage()
964 self.custom.dspval = Storage()
965 self.custom.inpval = Storage()
966 self.custom.label = Storage()
967 self.custom.comment = Storage()
968 self.custom.widget = Storage()
969 self.custom.linkto = Storage()
970
971 # default id field name
972 if not keyed:
973 self.id_field_name = table._id.name
974 else:
975 self.id_field_name = table._primarykey[0] # only works if one key
976
977 sep = separator or ''
978
979 for fieldname in self.fields:
980 if fieldname.find('.') >= 0:
981 continue
982
983 field = self.table[fieldname]
984 comment = None
985
986 if comments:
987 comment = col3.get(fieldname, field.comment)
988 if comment is None:
989 comment = ''
990 self.custom.comment[fieldname] = comment
991
992 if not labels is None and fieldname in labels:
993 label = labels[fieldname]
994 else:
995 label = field.label
996 self.custom.label[fieldname] = label
997
998 field_id = '%s_%s' % (table._tablename, fieldname)
999
1000 label = LABEL(label, label and sep, _for=field_id,
1001 _id=field_id + SQLFORM.ID_LABEL_SUFFIX)
1002
1003 row_id = field_id + SQLFORM.ID_ROW_SUFFIX
1004 if field.type == 'id':
1005 self.custom.dspval.id = nbsp
1006 self.custom.inpval.id = ''
1007 widget = ''
1008
1009 # store the id field name (for legacy databases)
1010 self.id_field_name = field.name
1011
1012 if record:
1013 if showid and field.name in record and field.readable:
1014 v = record[field.name]
1015 widget = SPAN(v, _id=field_id)
1016 self.custom.dspval.id = str(v)
1017 xfields.append((row_id, label, widget, comment))
1018 self.record_id = str(record[field.name])
1019 self.custom.widget.id = widget
1020 continue
1021
1022 if readonly and not ignore_rw and not field.readable:
1023 continue
1024
1025 if record:
1026 default = record[fieldname]
1027 else:
1028 default = field.default
1029 if isinstance(default, CALLABLETYPES):
1030 default = default()
1031
1032 cond = readonly or \
1033 (not ignore_rw and not field.writable and field.readable)
1034
1035 if default and not cond:
1036 default = field.formatter(default)
1037 dspval = default
1038 inpval = default
1039
1040 if cond:
1041
1042 # ## if field.represent is available else
1043 # ## ignore blob and preview uploaded images
1044 # ## format everything else
1045
1046 if field.represent:
1047 inp = represent(field, default, record)
1048 elif field.type in ['blob']:
1049 continue
1050 elif field.type == 'upload':
1051 inp = UploadWidget.represent(field, default, upload)
1052 elif field.type == 'boolean':
1053 inp = self.widgets.boolean.widget(
1054 field, default, _disabled=True)
1055 else:
1056 inp = field.formatter(default)
1057 elif field.type == 'upload':
1058 if field.widget:
1059 inp = field.widget(field, default, upload)
1060 else:
1061 inp = self.widgets.upload.widget(field, default, upload)
1062 elif field.widget:
1063 inp = field.widget(field, default)
1064 elif field.type == 'boolean':
1065 inp = self.widgets.boolean.widget(field, default)
1066 if default:
1067 inpval = 'checked'
1068 else:
1069 inpval = ''
1070 elif OptionsWidget.has_options(field):
1071 if not field.requires.multiple:
1072 inp = self.widgets.options.widget(field, default)
1073 else:
1074 inp = self.widgets.multiple.widget(field, default)
1075 if fieldname in keepopts:
1076 inpval = TAG[''](*inp.components)
1077 elif field.type.startswith('list:'):
1078 inp = self.widgets.list.widget(field, default)
1079 elif field.type == 'text':
1080 inp = self.widgets.text.widget(field, default)
1081 elif field.type == 'password':
1082 inp = self.widgets.password.widget(field, default)
1083 if self.record:
1084 dspval = PasswordWidget.DEFAULT_PASSWORD_DISPLAY
1085 else:
1086 dspval = ''
1087 elif field.type == 'blob':
1088 continue
1089 else:
1090 field_type = widget_class.match(str(field.type)).group()
1091 field_type = field_type in self.widgets and field_type or 'string'
1092 inp = self.widgets[field_type].widget(field, default)
1093
1094 xfields.append((row_id, label, inp, comment))
1095 self.custom.dspval[fieldname] = dspval or nbsp
1096 self.custom.inpval[
1097 fieldname] = inpval if not inpval is None else ''
1098 self.custom.widget[fieldname] = inp
1099
1100 # if a record is provided and found, as is linkto
1101 # build a link
1102 if record and linkto:
1103 db = linkto.split('/')[-1]
1104 for rfld in table._referenced_by:
1105 if keyed:
1106 query = urllib.quote('%s.%s==%s' % (
1107 db, rfld, record[rfld.type[10:].split('.')[1]]))
1108 else:
1109 query = urllib.quote(
1110 '%s.%s==%s' % (db, rfld, record[self.id_field_name]))
1111 lname = olname = '%s.%s' % (rfld.tablename, rfld.name)
1112 if ofields and not olname in ofields:
1113 continue
1114 if labels and lname in labels:
1115 lname = labels[lname]
1116 widget = A(lname,
1117 _class='reference',
1118 _href='%s/%s?query=%s' % (linkto, rfld.tablename, query))
1119 xfields.append(
1120 (olname.replace('.', '__') + SQLFORM.ID_ROW_SUFFIX,
1121 '', widget, col3.get(olname, '')))
1122 self.custom.linkto[olname.replace('.', '__')] = widget
1123 # </block>
1124
1125 # when deletable, add delete? checkbox
1126 self.custom.deletable = ''
1127 if record and deletable:
1128 widget = INPUT(_type='checkbox',
1129 _class='delete',
1130 _id=self.FIELDKEY_DELETE_RECORD,
1131 _name=self.FIELDNAME_REQUEST_DELETE,
1132 )
1133 xfields.append(
1134 (self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_ROW_SUFFIX,
1135 LABEL(
1136 delete_label, separator,
1137 _for=self.FIELDKEY_DELETE_RECORD,
1138 _id=self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_LABEL_SUFFIX),
1139 widget,
1140 col3.get(self.FIELDKEY_DELETE_RECORD, '')))
1141 self.custom.deletable = widget
1142
1143 # when writable, add submit button
1144 self.custom.submit = ''
1145 if not readonly:
1146 if 'submit' in buttons:
1147 widget = self.custom.submit = INPUT(_type='submit',
1148 _value=T(submit_button))
1149 elif buttons:
1150 widget = self.custom.submit = DIV(*buttons)
1151 if self.custom.submit:
1152 xfields.append(('submit_record' + SQLFORM.ID_ROW_SUFFIX,
1153 '', widget, col3.get('submit_button', '')))
1154
1155 # if a record is provided and found
1156 # make sure it's id is stored in the form
1157 if record:
1158 if not self['hidden']:
1159 self['hidden'] = {}
1160 if not keyed:
1161 self['hidden']['id'] = record[table._id.name]
1162
1163 (begin, end) = self._xml()
1164 self.custom.begin = XML("<%s %s>" % (self.tag, begin))
1165 self.custom.end = XML("%s</%s>" % (end, self.tag))
1166 table = self.createform(xfields)
1167 self.components = [table]
1168
1170 formstyle = self.formstyle
1171 if isinstance(formstyle, basestring):
1172 if formstyle in SQLFORM.formstyles:
1173 formstyle = SQLFORM.formstyles[formstyle]
1174 else:
1175 raise RuntimeError('formstyle not found')
1176
1177 if callable(formstyle):
1178 # backward compatibility, 4 argument function is the old style
1179 args, varargs, keywords, defaults = inspect.getargspec(formstyle)
1180 if defaults and len(args) - len(defaults) == 4 or len(args) == 4:
1181 table = TABLE()
1182 for id, a, b, c in xfields:
1183 newrows = formstyle(id, a, b, c)
1184 self.field_parent[id] = getattr(b, 'parent', None)
1185 if type(newrows).__name__ != "tuple":
1186 newrows = [newrows]
1187 for newrow in newrows:
1188 table.append(newrow)
1189 else:
1190 table = formstyle(self, xfields)
1191 for id, a, b, c in xfields:
1192 self.field_parent[id] = getattr(b, 'parent', None)
1193 else:
1194 raise RuntimeError('formstyle not supported')
1195 return table
1196
1197 - def accepts(
1198 self,
1199 request_vars,
1200 session=None,
1201 formname='%(tablename)s/%(record_id)s',
1202 keepvalues=None,
1203 onvalidation=None,
1204 dbio=True,
1205 hideerror=False,
1206 detect_record_change=False,
1207 **kwargs
1208 ):
1209
1210 """
1211 similar FORM.accepts but also does insert, update or delete in DAL.
1212 but if detect_record_change == True than:
1213 form.record_changed = False (record is properly validated/submitted)
1214 form.record_changed = True (record cannot be submitted because changed)
1215 elseif detect_record_change == False than:
1216 form.record_changed = None
1217 """
1218
1219 if keepvalues is None:
1220 keepvalues = True if self.record else False
1221
1222 if self.readonly:
1223 return False
1224
1225 if request_vars.__class__.__name__ == 'Request':
1226 request_vars = request_vars.post_vars
1227
1228 keyed = hasattr(self.table, '_primarykey')
1229
1230 # implement logic to detect whether record exist but has been modified
1231 # server side
1232 self.record_changed = None
1233 self.detect_record_change = detect_record_change
1234 if self.detect_record_change:
1235 if self.record:
1236 self.record_changed = False
1237 serialized = '|'.join(
1238 str(self.record[k]) for k in self.table.fields())
1239 self.record_hash = md5_hash(serialized)
1240
1241 # logic to deal with record_id for keyed tables
1242 if self.record:
1243 if keyed:
1244 formname_id = '.'.join(str(self.record[k])
1245 for k in self.table._primarykey
1246 if hasattr(self.record, k))
1247 record_id = dict((k, request_vars.get(k, None))
1248 for k in self.table._primarykey)
1249 else:
1250 (formname_id, record_id) = (self.record[self.id_field_name],
1251 request_vars.get('id', None))
1252 keepvalues = True
1253 else:
1254 if keyed:
1255 formname_id = 'create'
1256 record_id = dict([(k, None) for k in self.table._primarykey])
1257 else:
1258 (formname_id, record_id) = ('create', None)
1259
1260 if not keyed and isinstance(record_id, (list, tuple)):
1261 record_id = record_id[0]
1262
1263 if formname:
1264 formname = formname % dict(tablename=self.table._tablename,
1265 record_id=formname_id)
1266
1267 # ## THIS IS FOR UNIQUE RECORDS, read IS_NOT_IN_DB
1268
1269 for fieldname in self.fields:
1270 field = self.table[fieldname]
1271 requires = field.requires or []
1272 if not isinstance(requires, (list, tuple)):
1273 requires = [requires]
1274 [item.set_self_id(self.record_id) for item in requires
1275 if hasattr(item, 'set_self_id') and self.record_id]
1276
1277 # ## END
1278
1279 fields = {}
1280 for key in self.vars:
1281 fields[key] = self.vars[key]
1282
1283 ret = FORM.accepts(
1284 self,
1285 request_vars,
1286 session,
1287 formname,
1288 keepvalues,
1289 onvalidation,
1290 hideerror=hideerror,
1291 **kwargs
1292 )
1293
1294 self.deleted = \
1295 request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
1296
1297 self.custom.end = TAG[''](self.hidden_fields(), self.custom.end)
1298
1299 auch = record_id and self.errors and self.deleted
1300
1301 if self.record_changed and self.detect_record_change:
1302 message_onchange = \
1303 kwargs.setdefault("message_onchange",
1304 current.T("A record change was detected. " +
1305 "Consecutive update self-submissions " +
1306 "are not allowed. Try re-submitting or " +
1307 "refreshing the form page."))
1308 if message_onchange is not None:
1309 current.response.flash = message_onchange
1310 return ret
1311 elif (not ret) and (not auch):
1312 # auch is true when user tries to delete a record
1313 # that does not pass validation, yet it should be deleted
1314 for fieldname in self.fields:
1315 field = self.table[fieldname]
1316 ### this is a workaround! widgets should always have default not None!
1317 if not field.widget and field.type.startswith('list:') and \
1318 not OptionsWidget.has_options(field):
1319 field.widget = self.widgets.list.widget
1320 if field.widget and fieldname in request_vars:
1321 if fieldname in self.request_vars:
1322 value = self.request_vars[fieldname]
1323 elif self.record:
1324 value = self.record[fieldname]
1325 else:
1326 value = self.table[fieldname].default
1327 if field.type.startswith('list:') and isinstance(value, str):
1328 value = [value]
1329 row_id = '%s_%s%s' % (
1330 self.table, fieldname, SQLFORM.ID_ROW_SUFFIX)
1331 widget = field.widget(field, value)
1332 parent = self.field_parent[row_id]
1333 if parent:
1334 parent.components = [widget]
1335 if self.errors.get(fieldname):
1336 parent._traverse(False, hideerror)
1337 self.custom.widget[fieldname] = widget
1338 self.accepted = ret
1339 return ret
1340
1341 if record_id and str(record_id) != str(self.record_id):
1342 raise SyntaxError('user is tampering with form\'s record_id: '
1343 '%s != %s' % (record_id, self.record_id))
1344
1345 if record_id and dbio and not keyed:
1346 self.vars.id = self.record[self.id_field_name]
1347
1348 if self.deleted and self.custom.deletable:
1349 if dbio:
1350 if keyed:
1351 qry = reduce(lambda x, y: x & y,
1352 [self.table[k] == record_id[k]
1353 for k in self.table._primarykey])
1354 else:
1355 qry = self.table._id == self.record[self.id_field_name]
1356 self.table._db(qry).delete()
1357 self.errors.clear()
1358 for component in self.elements('input, select, textarea'):
1359 component['_disabled'] = True
1360 self.accepted = True
1361 return True
1362
1363 for fieldname in self.fields:
1364 if not fieldname in self.table.fields:
1365 continue
1366
1367 if not self.ignore_rw and not self.table[fieldname].writable:
1368 ### this happens because FORM has no knowledge of writable
1369 ### and thinks that a missing boolean field is a None
1370 if self.table[fieldname].type == 'boolean' and \
1371 self.vars.get(fieldname, True) is None:
1372 del self.vars[fieldname]
1373 continue
1374
1375 field = self.table[fieldname]
1376 if field.type == 'id':
1377 continue
1378 if field.type == 'boolean':
1379 if self.vars.get(fieldname, False):
1380 self.vars[fieldname] = fields[fieldname] = True
1381 else:
1382 self.vars[fieldname] = fields[fieldname] = False
1383 elif field.type == 'password' and self.record\
1384 and request_vars.get(fieldname, None) == \
1385 PasswordWidget.DEFAULT_PASSWORD_DISPLAY:
1386 continue # do not update if password was not changed
1387 elif field.type == 'upload':
1388 f = self.vars[fieldname]
1389 fd = '%s__delete' % fieldname
1390 if f == '' or f is None:
1391 if self.vars.get(fd, False):
1392 f = self.table[fieldname].default or ''
1393 fields[fieldname] = f
1394 elif self.record:
1395 if self.record[fieldname]:
1396 fields[fieldname] = self.record[fieldname]
1397 else:
1398 f = self.table[fieldname].default or ''
1399 fields[fieldname] = f
1400 else:
1401 fields[fieldname] = ''
1402 self.vars[fieldname] = fields[fieldname]
1403 if not f:
1404 continue
1405 else:
1406 f = os.path.join(current.request.folder,
1407 os.path.normpath(f))
1408 source_file = open(f, 'rb')
1409 original_filename = os.path.split(f)[1]
1410 elif hasattr(f, 'file'):
1411 (source_file, original_filename) = (f.file, f.filename)
1412 elif isinstance(f, (str, unicode)):
1413 ### do not know why this happens, it should not
1414 (source_file, original_filename) = \
1415 (cStringIO.StringIO(f), 'file.txt')
1416 newfilename = field.store(source_file, original_filename,
1417 field.uploadfolder)
1418 # this line was for backward compatibility but problematic
1419 # self.vars['%s_newfilename' % fieldname] = newfilename
1420 fields[fieldname] = newfilename
1421 if isinstance(field.uploadfield, str):
1422 fields[field.uploadfield] = source_file.read()
1423 # proposed by Hamdy (accept?) do we need fields at this point?
1424 self.vars[fieldname] = fields[fieldname]
1425 continue
1426 elif fieldname in self.vars:
1427 fields[fieldname] = self.vars[fieldname]
1428 elif field.default is None and field.type != 'blob':
1429 self.errors[fieldname] = 'no data'
1430 self.accepted = False
1431 return False
1432 value = fields.get(fieldname, None)
1433 if field.type == 'list:string':
1434 if not isinstance(value, (tuple, list)):
1435 fields[fieldname] = value and [value] or []
1436 elif isinstance(field.type, str) and field.type.startswith('list:'):
1437 if not isinstance(value, list):
1438 fields[fieldname] = [safe_int(
1439 x) for x in (value and [value] or [])]
1440 elif field.type == 'integer':
1441 if not value is None:
1442 fields[fieldname] = safe_int(value)
1443 elif field.type.startswith('reference'):
1444 if not value is None and isinstance(self.table, Table) and not keyed:
1445 fields[fieldname] = safe_int(value)
1446 elif field.type == 'double':
1447 if not value is None:
1448 fields[fieldname] = safe_float(value)
1449
1450 for fieldname in self.vars:
1451 if fieldname != 'id' and fieldname in self.table.fields\
1452 and not fieldname in fields and not fieldname\
1453 in request_vars:
1454 fields[fieldname] = self.vars[fieldname]
1455
1456 if dbio:
1457 if 'delete_this_record' in fields:
1458 # this should never happen but seems to happen to some
1459 del fields['delete_this_record']
1460 for field in self.table:
1461 if not field.name in fields and field.writable is False \
1462 and field.update is None and field.compute is None:
1463 if record_id and self.record:
1464 fields[field.name] = self.record[field.name]
1465 elif not self.table[field.name].default is None:
1466 fields[field.name] = self.table[field.name].default
1467 if keyed:
1468 if reduce(lambda x, y: x and y, record_id.values()): # if record_id
1469 if fields:
1470 qry = reduce(lambda x, y: x & y,
1471 [self.table[k] == self.record[k] for k in self.table._primarykey])
1472 self.table._db(qry).update(**fields)
1473 else:
1474 pk = self.table.insert(**fields)
1475 if pk:
1476 self.vars.update(pk)
1477 else:
1478 ret = False
1479 else:
1480 if record_id:
1481 self.vars.id = self.record[self.id_field_name]
1482 if fields:
1483 self.table._db(self.table._id == self.record[
1484 self.id_field_name]).update(**fields)
1485 else:
1486 self.vars.id = self.table.insert(**fields)
1487 self.accepted = ret
1488 return ret
1489
1490 AUTOTYPES = {
1491 type(''): ('string', None),
1492 type(True): ('boolean', None),
1493 type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
1494 type(1.0): ('double', IS_FLOAT_IN_RANGE()),
1495 type([]): ('list:string', None),
1496 type(datetime.date.today()): ('date', IS_DATE()),
1497 type(datetime.datetime.today()): ('datetime', IS_DATETIME())
1498 }
1499
1500 @staticmethod
1502 fields = []
1503 for key, value in sorted(dictionary.items()):
1504 t, requires = SQLFORM.AUTOTYPES.get(type(value), (None, None))
1505 if t:
1506 fields.append(Field(key, t, requires=requires,
1507 default=value))
1508 return SQLFORM.factory(*fields, **kwargs)
1509
1510 @staticmethod
1512 import os
1513 if query:
1514 session[name] = query.db(query).select().first().as_dict()
1515 elif os.path.exists(filename):
1516 env = {'datetime': datetime}
1517 session[name] = eval(open(filename).read(), {}, env)
1518 form = SQLFORM.dictform(session[name])
1519 if form.process().accepted:
1520 session[name].update(form.vars)
1521 if query:
1522 query.db(query).update(**form.vars)
1523 else:
1524 open(filename, 'w').write(repr(session[name]))
1525 return form
1526
1527 @staticmethod
1529 """
1530 generates a SQLFORM for the given fields.
1531
1532 Internally will build a non-database based data model
1533 to hold the fields.
1534 """
1535 # Define a table name, this way it can be logical to our CSS.
1536 # And if you switch from using SQLFORM to SQLFORM.factory
1537 # your same css definitions will still apply.
1538
1539 table_name = attributes.get('table_name', 'no_table')
1540
1541 # So it won't interfear with SQLDB.define_table
1542 if 'table_name' in attributes:
1543 del attributes['table_name']
1544
1545 return SQLFORM(DAL(None).define_table(table_name, *fields),
1546 **attributes)
1547
1548 @staticmethod
1550 request = current.request
1551 if isinstance(keywords, (tuple, list)):
1552 keywords = keywords[0]
1553 request.vars.keywords = keywords
1554 key = keywords.strip()
1555 if key and not ' ' in key and not '"' in key and not "'" in key:
1556 SEARCHABLE_TYPES = ('string', 'text', 'list:string')
1557 parts = [field.contains(
1558 key) for field in fields if field.type in SEARCHABLE_TYPES]
1559 else:
1560 parts = None
1561 if parts:
1562 return reduce(lambda a, b: a | b, parts)
1563 else:
1564 return smart_query(fields, key)
1565
1566 @staticmethod
1659
1660 @staticmethod
1661 - def grid(query,
1662 fields=None,
1663 field_id=None,
1664 left=None,
1665 headers={},
1666 orderby=None,
1667 groupby=None,
1668 searchable=True,
1669 sortable=True,
1670 paginate=20,
1671 deletable=True,
1672 editable=True,
1673 details=True,
1674 selectable=None,
1675 create=True,
1676 csv=True,
1677 links=None,
1678 links_in_grid=True,
1679 upload='<default>',
1680 args=[],
1681 user_signature=True,
1682 maxtextlengths={},
1683 maxtextlength=20,
1684 onvalidation=None,
1685 oncreate=None,
1686 onupdate=None,
1687 ondelete=None,
1688 sorter_icons=(XML('↑'), XML('↓')),
1689 ui = 'web2py',
1690 showbuttontext=True,
1691 _class="web2py_grid",
1692 formname='web2py_grid',
1693 search_widget='default',
1694 ignore_rw = False,
1695 formstyle = 'table3cols',
1696 exportclasses = None,
1697 formargs={},
1698 createargs={},
1699 editargs={},
1700 viewargs={},
1701 buttons_placement = 'right',
1702 links_placement = 'right'
1703 ):
1704
1705 # jQuery UI ThemeRoller classes (empty if ui is disabled)
1706 if ui == 'jquery-ui':
1707 ui = dict(widget='ui-widget',
1708 header='ui-widget-header',
1709 content='ui-widget-content',
1710 default='ui-state-default',
1711 cornerall='ui-corner-all',
1712 cornertop='ui-corner-top',
1713 cornerbottom='ui-corner-bottom',
1714 button='ui-button-text-icon-primary',
1715 buttontext='ui-button-text',
1716 buttonadd='ui-icon ui-icon-plusthick',
1717 buttonback='ui-icon ui-icon-arrowreturnthick-1-w',
1718 buttonexport='ui-icon ui-icon-transferthick-e-w',
1719 buttondelete='ui-icon ui-icon-trash',
1720 buttonedit='ui-icon ui-icon-pencil',
1721 buttontable='ui-icon ui-icon-triangle-1-e',
1722 buttonview='ui-icon ui-icon-zoomin',
1723 )
1724 elif ui == 'web2py':
1725 ui = dict(widget='',
1726 header='',
1727 content='',
1728 default='',
1729 cornerall='',
1730 cornertop='',
1731 cornerbottom='',
1732 button='button btn',
1733 buttontext='buttontext button',
1734 buttonadd='icon plus icon-plus',
1735 buttonback='icon leftarrow icon-arrow-left',
1736 buttonexport='icon downarrow icon-download',
1737 buttondelete='icon trash icon-trash',
1738 buttonedit='icon pen icon-pencil',
1739 buttontable='icon rightarrow icon-arrow-right',
1740 buttonview='icon magnifier icon-zoom-in',
1741 )
1742 elif not isinstance(ui, dict):
1743 raise RuntimeError('SQLFORM.grid ui argument must be a dictionary')
1744
1745 db = query._db
1746 T = current.T
1747 request = current.request
1748 session = current.session
1749 response = current.response
1750 logged = session.auth and session.auth.user
1751 wenabled = (not user_signature or logged)
1752 create = wenabled and create
1753 editable = wenabled and editable
1754 deletable = wenabled and deletable
1755
1756 def url(**b):
1757 b['args'] = args + b.get('args', [])
1758 localvars = request.vars.copy()
1759 localvars.update(b.get('vars', {}))
1760 b['vars'] = localvars
1761 b['hash_vars'] = False
1762 b['user_signature'] = user_signature
1763 return URL(**b)
1764
1765 def url2(**b):
1766 b['args'] = request.args + b.get('args', [])
1767 localvars = request.vars.copy()
1768 localvars.update(b.get('vars', {}))
1769 b['vars'] = localvars
1770 b['hash_vars'] = False
1771 b['user_signature'] = user_signature
1772 return URL(**b)
1773
1774 referrer = session.get('_web2py_grid_referrer_' + formname, url())
1775 # if not user_signature every action is accessible
1776 # else forbid access unless
1777 # - url is based url
1778 # - url has valid signature (vars are not signed, only path_info)
1779 # = url does not contain 'create','delete','edit' (readonly)
1780 if user_signature:
1781 if not (
1782 '/'.join(str(a) for a in args) == '/'.join(request.args) or
1783 URL.verify(request,user_signature=user_signature,
1784 hash_vars=False) or
1785 (request.args(len(args))=='view' and not logged)):
1786 session.flash = T('not authorized')
1787 redirect(referrer)
1788
1789 def gridbutton(buttonclass='buttonadd', buttontext='Add',
1790 buttonurl=url(args=[]), callback=None,
1791 delete=None, trap=True):
1792 if showbuttontext:
1793 if callback:
1794 return A(SPAN(_class=ui.get(buttonclass)),
1795 SPAN(T(buttontext), _title=buttontext,
1796 _class=ui.get('buttontext')),
1797 callback=callback, delete=delete,
1798 _class=trap_class(ui.get('button'), trap))
1799 else:
1800 return A(SPAN(_class=ui.get(buttonclass)),
1801 SPAN(T(buttontext), _title=buttontext,
1802 _class=ui.get('buttontext')),
1803 _href=buttonurl,
1804 _class=trap_class(ui.get('button'), trap))
1805 else:
1806 if callback:
1807 return A(SPAN(_class=ui.get(buttonclass)),
1808 callback=callback, delete=delete,
1809 _title=buttontext,
1810 _class=trap_class(ui.get('buttontext'), trap))
1811 else:
1812 return A(SPAN(_class=ui.get(buttonclass)),
1813 _href=buttonurl, _title=buttontext,
1814 _class=trap_class(ui.get('buttontext'), trap))
1815 dbset = db(query)
1816 tablenames = db._adapter.tables(dbset.query)
1817 if left is not None:
1818 tablenames += db._adapter.tables(left)
1819 tables = [db[tablename] for tablename in tablenames]
1820 if not fields:
1821 fields = reduce(lambda a, b: a + b,
1822 [[field for field in table] for table in tables])
1823 if not field_id:
1824 field_id = tables[0]._id
1825 columns = [str(field) for field in fields
1826 if field._tablename in tablenames]
1827
1828 if not str(field_id) in [str(f) for f in fields]:
1829 fields.append(field_id)
1830 table = field_id.table
1831 tablename = table._tablename
1832 if upload == '<default>':
1833 upload = lambda filename: url(args=['download', filename])
1834 if len(request.args) > 1 and request.args[-2] == 'download':
1835 stream = response.download(request, db)
1836 raise HTTP(200, stream, **response.headers)
1837
1838 def buttons(edit=False, view=False, record=None):
1839 buttons = DIV(gridbutton('buttonback', 'Back', referrer),
1840 _class='form_header row_buttons %(header)s %(cornertop)s' % ui)
1841 if edit and (not callable(edit) or edit(record)):
1842 args = ['edit', table._tablename, request.args[-1]]
1843 buttons.append(gridbutton('buttonedit', 'Edit',
1844 url(args=args)))
1845 if view:
1846 args = ['view', table._tablename, request.args[-1]]
1847 buttons.append(gridbutton('buttonview', 'View',
1848 url(args=args)))
1849 if record and links:
1850 for link in links:
1851 if isinstance(link, dict):
1852 buttons.append(link['body'](record))
1853 elif link(record):
1854 buttons.append(link(record))
1855 return buttons
1856
1857 def linsert(lst, i, x):
1858 """
1859 a = [1,2]
1860 linsert(a, 1, [0,3])
1861 a = [1, 0, 3, 2]
1862 """
1863 lst[i:i] = x
1864
1865 formfooter = DIV(
1866 _class='form_footer row_buttons %(header)s %(cornerbottom)s' % ui)
1867
1868 create_form = update_form = view_form = search_form = None
1869 sqlformargs = dict(formargs)
1870
1871 if create and len(request.args) > 1 and request.args[-2] == 'new':
1872 table = db[request.args[-1]]
1873 sqlformargs.update(createargs)
1874 create_form = SQLFORM(
1875 table, ignore_rw=ignore_rw, formstyle=formstyle,
1876 _class='web2py_form',
1877 **sqlformargs)
1878 create_form.process(formname=formname,
1879 next=referrer,
1880 onvalidation=onvalidation,
1881 onsuccess=oncreate)
1882 res = DIV(buttons(), create_form, formfooter, _class=_class)
1883 res.create_form = create_form
1884 res.update_form = update_form
1885 res.view_form = view_form
1886 res.search_form = search_form
1887 return res
1888
1889 elif details and len(request.args) > 2 and request.args[-3] == 'view':
1890 table = db[request.args[-2]]
1891 record = table(request.args[-1]) or redirect(referrer)
1892 sqlformargs.update(viewargs)
1893 view_form = SQLFORM(
1894 table, record, upload=upload, ignore_rw=ignore_rw,
1895 formstyle=formstyle, readonly=True, _class='web2py_form',
1896 **sqlformargs)
1897 res = DIV(buttons(edit=editable, record=record), view_form,
1898 formfooter, _class=_class)
1899 res.create_form = create_form
1900 res.update_form = update_form
1901 res.view_form = view_form
1902 res.search_form = search_form
1903 return res
1904 elif editable and len(request.args) > 2 and request.args[-3] == 'edit':
1905 table = db[request.args[-2]]
1906 record = table(request.args[-1]) or redirect(URL('error'))
1907 sqlformargs.update(editargs)
1908 update_form = SQLFORM(
1909 table,
1910 record, upload=upload, ignore_rw=ignore_rw,
1911 formstyle=formstyle, deletable=deletable,
1912 _class='web2py_form',
1913 submit_button=T('Submit'),
1914 delete_label=T('Check to delete'),
1915 **sqlformargs)
1916 update_form.process(
1917 formname=formname,
1918 onvalidation=onvalidation,
1919 onsuccess=onupdate,
1920 next=referrer)
1921 res = DIV(buttons(view=details, record=record),
1922 update_form, formfooter, _class=_class)
1923 res.create_form = create_form
1924 res.update_form = update_form
1925 res.view_form = view_form
1926 res.search_form = search_form
1927 return res
1928 elif deletable and len(request.args) > 2 and request.args[-3] == 'delete':
1929 table = db[request.args[-2]]
1930 if ondelete:
1931 ondelete(table, request.args[-1])
1932 ret = db(table[table._id.name] == request.args[-1]).delete()
1933 return ret
1934
1935 exportManager = dict(
1936 csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'),
1937 csv=(ExporterCSV, 'CSV'),
1938 xml=(ExporterXML, 'XML'),
1939 html=(ExporterHTML, 'HTML'),
1940 tsv_with_hidden_cols=
1941 (ExporterTSV, 'TSV (Excel compatible, hidden cols)'),
1942 tsv=(ExporterTSV, 'TSV (Excel compatible)'))
1943 if not exportclasses is None:
1944 """
1945 remember: allow to set exportclasses=dict(csv=False) to disable the csv format
1946 """
1947 exportManager.update(exportclasses)
1948
1949 export_type = request.vars._export_type
1950 if export_type:
1951 order = request.vars.order or ''
1952 if sortable:
1953 if order and not order == 'None':
1954 if order[:1] == '~':
1955 sign, rorder = '~', order[1:]
1956 else:
1957 sign, rorder = '', order
1958 tablename, fieldname = rorder.split('.', 1)
1959 orderby = db[tablename][fieldname]
1960 if sign == '~':
1961 orderby = ~orderby
1962
1963 expcolumns = columns
1964 if export_type.endswith('with_hidden_cols'):
1965 expcolumns = [f for f in fields if f._tablename in tablenames]
1966 if export_type in exportManager and exportManager[export_type]:
1967 if request.vars.keywords:
1968 try:
1969 dbset = dbset(SQLFORM.build_query(
1970 fields, request.vars.get('keywords', '')))
1971 rows = dbset.select(cacheable=True, *expcolumns)
1972 except Exception, e:
1973 response.flash = T('Internal Error')
1974 rows = []
1975 else:
1976 rows = dbset.select(left=left, orderby=orderby,
1977 cacheable=True, *expcolumns)
1978
1979 value = exportManager[export_type]
1980 clazz = value[0] if hasattr(value, '__getitem__') else value
1981 oExp = clazz(rows)
1982 filename = '.'.join(('rows', oExp.file_ext))
1983 response.headers['Content-Type'] = oExp.content_type
1984 response.headers['Content-Disposition'] = \
1985 'attachment;filename=' + filename + ';'
1986 raise HTTP(200, oExp.export(), **response.headers)
1987
1988 elif request.vars.records and not isinstance(
1989 request.vars.records, list):
1990 request.vars.records = [request.vars.records]
1991 elif not request.vars.records:
1992 request.vars.records = []
1993
1994 session['_web2py_grid_referrer_' + formname] = url2(vars=request.vars)
1995 console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui)
1996 error = None
1997 if create:
1998 add = gridbutton(
1999 buttonclass='buttonadd',
2000 buttontext='Add',
2001 buttonurl=url(args=['new', tablename]))
2002 if not searchable:
2003 console.append(add)
2004 else:
2005 add = ''
2006
2007 if searchable:
2008 sfields = reduce(lambda a, b: a + b,
2009 [[f for f in t if f.readable] for t in tables])
2010 if isinstance(search_widget, dict):
2011 search_widget = search_widget[tablename]
2012 if search_widget == 'default':
2013 prefix = formname == 'web2py_grid' and 'w2p' or 'w2p_%s' % formname
2014 search_menu = SQLFORM.search_menu(sfields, prefix=prefix)
2015 spanel_id = '%s_query_fields' % prefix
2016 sfields_id = '%s_query_panel' % prefix
2017 skeywords_id = '%s_keywords' % prefix
2018 search_widget = lambda sfield, url: CAT(FORM(
2019 INPUT(_name='keywords', _value=request.vars.keywords,
2020 _id=skeywords_id,
2021 _onfocus="jQuery('#%s').change();jQuery('#%s').slideDown();" % (spanel_id, sfields_id)),
2022 INPUT(_type='submit', _value=T('Search'), _class="btn"),
2023 INPUT(_type='submit', _value=T('Clear'), _class="btn",
2024 _onclick="jQuery('#%s').val('');" % skeywords_id),
2025 _method="GET", _action=url), search_menu)
2026 form = search_widget and search_widget(sfields, url()) or ''
2027 console.append(add)
2028 console.append(form)
2029 keywords = request.vars.get('keywords', '')
2030 try:
2031 if callable(searchable):
2032 subquery = searchable(sfields, keywords)
2033 else:
2034 subquery = SQLFORM.build_query(sfields, keywords)
2035 except RuntimeError:
2036 subquery = None
2037 error = T('Invalid query')
2038 else:
2039 subquery = None
2040
2041 if subquery:
2042 dbset = dbset(subquery)
2043 try:
2044 if left or groupby:
2045 c = 'count(*)'
2046 nrows = dbset.select(c, left=left, cacheable=True,
2047 groupby=groupby).first()[c]
2048 elif dbset._db._adapter.dbengine=='google:datastore':
2049 #if we don't set a limit, this can timeout for a large table
2050 nrows = dbset.db._adapter.count(dbset.query, limit=1000)
2051 else:
2052 nrows = dbset.count()
2053 except:
2054 nrows = 0
2055 error = T('Unsupported query')
2056
2057 order = request.vars.order or ''
2058 if sortable:
2059 if order and not order == 'None':
2060 tablename, fieldname = order.split('~')[-1].split('.', 1)
2061 sort_field = db[tablename][fieldname]
2062 exception = sort_field.type in ('date', 'datetime', 'time')
2063 if exception:
2064 orderby = (order[:1] == '~' and sort_field) or ~sort_field
2065 else:
2066 orderby = (order[:1] == '~' and ~sort_field) or sort_field
2067
2068 headcols = []
2069 if selectable:
2070 headcols.append(TH(_class=ui.get('default')))
2071 for field in fields:
2072 if columns and not str(field) in columns:
2073 continue
2074 if not field.readable:
2075 continue
2076 key = str(field)
2077 header = headers.get(str(field), field.label or key)
2078 if sortable:
2079 if key == order:
2080 key, marker = '~' + order, sorter_icons[0]
2081 elif key == order[1:]:
2082 marker = sorter_icons[1]
2083 else:
2084 marker = ''
2085 header = A(header, marker, _href=url(vars=dict(
2086 keywords=request.vars.keywords or '',
2087 order=key)), _class=trap_class())
2088 headcols.append(TH(header, _class=ui.get('default')))
2089
2090 toadd = []
2091 if links and links_in_grid:
2092 for link in links:
2093 if isinstance(link, dict):
2094 toadd.append(TH(link['header'], _class=ui.get('default')))
2095 if links_placement in ['right', 'both']:
2096 headcols.extend(toadd)
2097 if links_placement in ['left', 'both']:
2098 linsert(headcols, 0, toadd)
2099
2100 # Include extra column for buttons if needed.
2101 include_buttons_column = (details or editable or deletable or
2102 (links and links_in_grid and
2103 not all([isinstance(link, dict) for link in links])))
2104 if include_buttons_column:
2105 if buttons_placement in ['right', 'both']:
2106 headcols.append(TH(_class=ui.get('default','')))
2107 if buttons_placement in ['left', 'both']:
2108 headcols.insert(0, TH(_class=ui.get('default','')))
2109
2110 head = TR(*headcols, **dict(_class=ui.get('header')))
2111
2112 cursor = True
2113 #figure out what page we are one to setup the limitby
2114 if paginate and dbset._db._adapter.dbengine=='google:datastore':
2115 cursor = request.vars.cursor or True
2116 limitby = (0, paginate)
2117 try: page = int(request.vars.page or 1)-1
2118 except ValueError: page = 0
2119 elif paginate and paginate<nrows:
2120 try: page = int(request.vars.page or 1)-1
2121 except ValueError: page = 0
2122 limitby = (paginate*page,paginate*(page+1))
2123 else:
2124 limitby = None
2125
2126 try:
2127 table_fields = [f for f in fields if f._tablename in tablenames]
2128 if dbset._db._adapter.dbengine=='google:datastore':
2129 rows = dbset.select(left=left,orderby=orderby,
2130 groupby=groupby,limitby=limitby,
2131 reusecursor=cursor,
2132 cacheable=True,*table_fields)
2133 next_cursor = dbset._db.get('_lastcursor', None)
2134 else:
2135 rows = dbset.select(left=left,orderby=orderby,
2136 groupby=groupby,limitby=limitby,
2137 cacheable=True,*table_fields)
2138 except SyntaxError:
2139 rows = None
2140 next_cursor = None
2141 error = T("Query Not Supported")
2142 except Exception, e:
2143 rows = None
2144 next_cursor = None
2145 error = T("Query Not Supported: %s")%e
2146
2147 message = error
2148 if not message and nrows:
2149 if dbset._db._adapter.dbengine=='google:datastore' and nrows>=1000:
2150 message = T('at least %(nrows)s records found') % dict(nrows=nrows)
2151 else:
2152 message = T('%(nrows)s records found') % dict(nrows=nrows)
2153 console.append(DIV(message,_class='web2py_counter'))
2154
2155 paginator = UL()
2156 if paginate and dbset._db._adapter.dbengine=='google:datastore':
2157 #this means we may have a large table with an unknown number of rows.
2158 try:
2159 page = int(request.vars.page or 1)-1
2160 except ValueError:
2161 page = 0
2162 paginator.append(LI('page %s'%(page+1)))
2163 if next_cursor:
2164 d = dict(page=page+2, cursor=next_cursor)
2165 if order: d['order']=order
2166 if request.vars.keywords: d['keywords']=request.vars.keywords
2167 paginator.append(LI(
2168 A('next',_href=url(vars=d),_class=trap_class())))
2169 elif paginate and paginate<nrows:
2170 npages, reminder = divmod(nrows, paginate)
2171 if reminder:
2172 npages += 1
2173 try:
2174 page = int(request.vars.page or 1) - 1
2175 except ValueError:
2176 page = 0
2177
2178 def self_link(name, p):
2179 d = dict(page=p + 1)
2180 if order:
2181 d['order'] = order
2182 if request.vars.keywords:
2183 d['keywords'] = request.vars.keywords
2184 return A(name, _href=url(vars=d), _class=trap_class())
2185 NPAGES = 5 # window is 2*NPAGES
2186 if page > NPAGES + 1:
2187 paginator.append(LI(self_link('<<', 0)))
2188 if page > NPAGES:
2189 paginator.append(LI(self_link('<', page - 1)))
2190 pages = range(max(0, page - NPAGES), min(page + NPAGES, npages))
2191 for p in pages:
2192 if p == page:
2193 paginator.append(LI(A(p + 1, _onclick='return false'),
2194 _class=trap_class('current')))
2195 else:
2196 paginator.append(LI(self_link(p + 1, p)))
2197 if page < npages - NPAGES:
2198 paginator.append(LI(self_link('>', page + 1)))
2199 if page < npages - NPAGES - 1:
2200 paginator.append(LI(self_link('>>', npages - 1)))
2201 else:
2202 limitby = None
2203
2204 if rows:
2205 htmltable = TABLE(THEAD(head))
2206 tbody = TBODY()
2207 numrec = 0
2208 for row in rows:
2209 trcols = []
2210 id = row[field_id]
2211 if selectable:
2212 trcols.append(
2213 INPUT(_type="checkbox", _name="records", _value=id,
2214 value=request.vars.records))
2215 for field in fields:
2216 if not str(field) in columns:
2217 continue
2218 if not field.readable:
2219 continue
2220 if field.type == 'blob':
2221 continue
2222 value = row[field]
2223 maxlength = maxtextlengths.get(str(field), maxtextlength)
2224 if field.represent:
2225 try:
2226 value = field.represent(value, row)
2227 except KeyError:
2228 try:
2229 value = field.represent(
2230 value, row[field._tablename])
2231 except KeyError:
2232 pass
2233 elif field.type == 'boolean':
2234 value = INPUT(_type="checkbox", _checked=value,
2235 _disabled=True)
2236 elif field.type == 'upload':
2237 if value:
2238 if callable(upload):
2239 value = A(
2240 current.T('file'), _href=upload(value))
2241 elif upload:
2242 value = A(current.T('file'),
2243 _href='%s/%s' % (upload, value))
2244 else:
2245 value = ''
2246 if isinstance(value, str):
2247 value = truncate_string(value, maxlength)
2248 elif not isinstance(value, DIV):
2249 value = field.formatter(value)
2250 trcols.append(TD(value))
2251 row_buttons = TD(_class='row_buttons')
2252 if links and links_in_grid:
2253 toadd = []
2254 for link in links:
2255 if isinstance(link, dict):
2256 toadd.append(TD(link['body'](row)))
2257 else:
2258 if link(row):
2259 row_buttons.append(link(row))
2260 if links_placement in ['right', 'both']:
2261 trcols.extend(toadd)
2262 if links_placement in ['left', 'both']:
2263 linsert(trcols, 0, toadd)
2264
2265 if include_buttons_column:
2266 if details and (not callable(details) or details(row)):
2267 row_buttons.append(gridbutton(
2268 'buttonview', 'View',
2269 url(args=['view', tablename, id])))
2270 if editable and (not callable(editable) or editable(row)):
2271 row_buttons.append(gridbutton(
2272 'buttonedit', 'Edit',
2273 url(args=['edit', tablename, id])))
2274 if deletable and (not callable(deletable) or deletable(row)):
2275 row_buttons.append(gridbutton(
2276 'buttondelete', 'Delete',
2277 callback=url(args=['delete', tablename, id]),
2278 delete='tr'))
2279 if buttons_placement in ['right', 'both']:
2280 trcols.append(row_buttons)
2281 if buttons_placement in ['left', 'both']:
2282 trcols.insert(0, row_buttons)
2283 if numrec % 2 == 0:
2284 classtr = 'even'
2285 else:
2286 classtr = 'odd'
2287 numrec += 1
2288 if id:
2289 rid = id
2290 if callable(rid): # can this ever be callable?
2291 rid = rid(row)
2292 tr = TR(*trcols, **dict(
2293 _id=rid,
2294 _class='%s %s' % (classtr, 'with_id')))
2295 else:
2296 tr = TR(*trcols, **dict(_class=classtr))
2297 tbody.append(tr)
2298 htmltable.append(tbody)
2299 htmltable = DIV(htmltable, _style='width:100%;overflow-x:auto')
2300 if selectable:
2301 htmltable = FORM(htmltable, INPUT(_type="submit"))
2302 if htmltable.process(formname=formname).accepted:
2303 htmltable.vars.records = htmltable.vars.records or []
2304 htmltable.vars.records = htmltable.vars.records if type(htmltable.vars.records) == list else [htmltable.vars.records]
2305 records = [int(r) for r in htmltable.vars.records]
2306 selectable(records)
2307 redirect(referrer)
2308 else:
2309 htmltable = DIV(current.T('No records found'))
2310
2311 if csv and nrows:
2312 export_links = []
2313 for k, v in sorted(exportManager.items()):
2314 if not v:
2315 continue
2316 label = v[1] if hasattr(v, "__getitem__") else k
2317 link = url2(vars=dict(
2318 order=request.vars.order or '',
2319 _export_type=k,
2320 keywords=request.vars.keywords or ''))
2321 export_links.append(A(T(label), _href=link))
2322 export_menu = \
2323 DIV(T('Export:'), _class="w2p_export_menu", *export_links)
2324 else:
2325 export_menu = None
2326
2327 res = DIV(console, DIV(htmltable, _class="web2py_table"),
2328 _class='%s %s' % (_class, ui.get('widget')))
2329 if paginator.components:
2330 res.append(
2331 DIV(paginator,
2332 _class="web2py_paginator %(header)s %(cornerbottom)s" % ui))
2333 if export_menu:
2334 res.append(export_menu)
2335 res.create_form = create_form
2336 res.update_form = update_form
2337 res.view_form = view_form
2338 res.search_form = search_form
2339 return res
2340
2341 @staticmethod
2342 - def smartgrid(table, constraints=None, linked_tables=None,
2343 links=None, links_in_grid=True,
2344 args=None, user_signature=True,
2345 divider='>', breadcrumbs_class='',
2346 **kwargs):
2347 """
2348 @auth.requires_login()
2349 def index():
2350 db.define_table('person',Field('name'),format='%(name)s')
2351 db.define_table('dog',
2352 Field('name'),Field('owner',db.person),format='%(name)s')
2353 db.define_table('comment',Field('body'),Field('dog',db.dog))
2354 if db(db.person).isempty():
2355 from gluon.contrib.populate import populate
2356 populate(db.person,300)
2357 populate(db.dog,300)
2358 populate(db.comment,1000)
2359 db.commit()
2360 form=SQLFORM.smartgrid(db[request.args(0) or 'person']) #***
2361 return dict(form=form)
2362
2363 *** builds a complete interface to navigate all tables links
2364 to the request.args(0)
2365 table: pagination, search, view, edit, delete,
2366 children, parent, etc.
2367
2368 constraints is a dict {'table':query} that limits which
2369 records can be accessible
2370 links is a dict like
2371 {'tablename':[lambda row: A(....), ...]}
2372 that will add buttons when table tablename is displayed
2373 linked_tables is a optional list of tablenames of tables
2374 to be linked
2375 """
2376 request, T = current.request, current.T
2377 if args is None:
2378 args = []