| 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*')
48
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
70
77
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
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
149 _class = 'integer'
150
153 _class = 'double'
154
157 _class = 'decimal'
158
161 _class = 'time'
162
165 _class = 'date'
166
169 _class = 'datetime'
170
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
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
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
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 if ($(e.target).parent().is(':visible')) {
273 //make sure we didn't delete the element before we insert after
274 new_line.insertAfter($(e.target).parent());
275 } else {
276 //the line we clicked on was deleted, just add to end of list
277 new_line.appendTo(ul);
278 }
279 new_line.find(":text").focus();
280 return false;
281 }
282 function rl(ul, e) {
283 if (jQuery(ul).children().length > 1) {
284 //only remove if we have more than 1 item so the list is never empty
285 $(e.target).parent().remove();
286 }
287 }
288 function ml(ul) {
289 var line = jQuery(ul).find("li:first").clone(true);
290 line.find(':text').val('');
291 return line;
292 }
293 function rel(ul) {
294 jQuery(ul).find("li").each(function() {
295 var trimmed = jQuery.trim(jQuery(this.firstChild).val());
296 if (trimmed=='') jQuery(this).remove(); else jQuery(this.firstChild).val(trimmed);
297 });
298 }
299 })();
300 jQuery(document).ready(function(){jQuery('#%s_grow_input').grow_input();});
301 """ % _id)
302 attributes['_id'] = _id + '_grow_input'
303 attributes['_style'] = 'list-style:none'
304 return TAG[''](UL(*items, **attributes), script)
305
308
309 @classmethod
311 """
312 generates a SELECT tag, including OPTIONs (multiple options allowed)
313
314 see also: :meth:`FormWidget.widget`
315
316 :param size: optional param (default=5) to indicate how many rows must
317 be shown
318 """
319
320 attributes.update(_size=size, _multiple=True)
321
322 return OptionsWidget.widget(field, value, **attributes)
323
326
327 @classmethod
329 """
330 generates a TABLE tag, including INPUT radios (only 1 option allowed)
331
332 see also: :meth:`FormWidget.widget`
333 """
334
335 attr = cls._attributes(field, {}, **attributes)
336 attr['_class'] = attr.get('_class', 'web2py_radiowidget')
337
338 requires = field.requires
339 if not isinstance(requires, (list, tuple)):
340 requires = [requires]
341 if requires:
342 if hasattr(requires[0], 'options'):
343 options = requires[0].options()
344 else:
345 raise SyntaxError('widget cannot determine options of %s'
346 % field)
347 options = [(k, v) for k, v in options if str(v)]
348 opts = []
349 cols = attributes.get('cols', 1)
350 totals = len(options)
351 mods = totals % cols
352 rows = totals / cols
353 if mods:
354 rows += 1
355
356 #widget style
357 wrappers = dict(
358 table=(TABLE, TR, TD),
359 ul=(DIV, UL, LI),
360 divs=(CAT, DIV, DIV)
361 )
362 parent, child, inner = wrappers[attributes.get('style', 'table')]
363
364 for r_index in range(rows):
365 tds = []
366 for k, v in options[r_index * cols:(r_index + 1) * cols]:
367 checked = {'_checked': 'checked'} if k == value else {}
368 tds.append(inner(INPUT(_type='radio',
369 _id='%s%s' % (field.name, k),
370 _name=field.name,
371 requires=attr.get('requires', None),
372 hideerror=True, _value=k,
373 value=value,
374 **checked),
375 LABEL(v, _for='%s%s' % (field.name, k))))
376 opts.append(child(tds))
377
378 if opts:
379 opts[-1][0][0]['hideerror'] = False
380 return parent(*opts, **attr)
381
384
385 @classmethod
387 """
388 generates a TABLE tag, including INPUT checkboxes (multiple allowed)
389
390 see also: :meth:`FormWidget.widget`
391 """
392
393 # was values = re.compile('[\w\-:]+').findall(str(value))
394 if isinstance(value, (list, tuple)):
395 values = [str(v) for v in value]
396 else:
397 values = [str(value)]
398
399 attr = cls._attributes(field, {}, **attributes)
400 attr['_class'] = attr.get('_class', 'web2py_checkboxeswidget')
401
402 requires = field.requires
403 if not isinstance(requires, (list, tuple)):
404 requires = [requires]
405 if requires and hasattr(requires[0], 'options'):
406 options = requires[0].options()
407 else:
408 raise SyntaxError('widget cannot determine options of %s'
409 % field)
410
411 options = [(k, v) for k, v in options if k != '']
412 opts = []
413 cols = attributes.get('cols', 1)
414 totals = len(options)
415 mods = totals % cols
416 rows = totals / cols
417 if mods:
418 rows += 1
419
420 #widget style
421 wrappers = dict(
422 table=(TABLE, TR, TD),
423 ul=(DIV, UL, LI),
424 divs=(CAT, DIV, DIV)
425 )
426 parent, child, inner = wrappers[attributes.get('style', 'table')]
427
428 for r_index in range(rows):
429 tds = []
430 for k, v in options[r_index * cols:(r_index + 1) * cols]:
431 if k in values:
432 r_value = k
433 else:
434 r_value = []
435 tds.append(inner(INPUT(_type='checkbox',
436 _id='%s%s' % (field.name, k),
437 _name=field.name,
438 requires=attr.get('requires', None),
439 hideerror=True, _value=k,
440 value=r_value),
441 LABEL(v, _for='%s%s' % (field.name, k))))
442 opts.append(child(tds))
443
444 if opts:
445 opts.append(
446 INPUT(_class="hidden", requires=attr.get('requires', None),
447 _disabled="disabled", _name=field.name,
448 hideerror=False))
449 return parent(*opts, **attr)
450
453 _class = 'password'
454
455 DEFAULT_PASSWORD_DISPLAY = 8 * ('*')
456
457 @classmethod
459 """
460 generates a INPUT password tag.
461 If a value is present it will be shown as a number of '*', not related
462 to the length of the actual value.
463
464 see also: :meth:`FormWidget.widget`
465 """
466 # detect if attached a IS_STRONG with entropy
467 default = dict(
468 _type='password',
469 _value=(value and cls.DEFAULT_PASSWORD_DISPLAY) or '',
470 )
471 attr = cls._attributes(field, default, **attributes)
472 output = CAT(INPUT(**attr))
473
474 # deal with entropy check!
475 requires = field.requires
476 if not isinstance(requires, (list, tuple)):
477 requires = [requires]
478 is_strong = [r for r in requires if isinstance(r, IS_STRONG)]
479 if is_strong:
480 output.append(SCRIPT("web2py_validate_entropy(jQuery('#%s'),%s);" % (
481 attr['_id'], is_strong[0].entropy
482 if is_strong[0].entropy else "null")))
483 # end entropy check
484 return output
485
488 _class = 'upload'
489
490 DEFAULT_WIDTH = '150px'
491 ID_DELETE_SUFFIX = '__delete'
492 GENERIC_DESCRIPTION = 'file'
493 DELETE_FILE = 'delete'
494
495 @classmethod
497 """
498 generates a INPUT file tag.
499
500 Optionally provides an A link to the file, including a checkbox so
501 the file can be deleted.
502 All is wrapped in a DIV.
503
504 see also: :meth:`FormWidget.widget`
505
506 :param download_url: Optional URL to link to the file (default = None)
507 """
508
509 default = dict(_type='file',)
510 attr = cls._attributes(field, default, **attributes)
511
512 inp = INPUT(**attr)
513
514 if download_url and value:
515 if callable(download_url):
516 url = download_url(value)
517 else:
518 url = download_url + '/' + value
519 (br, image) = ('', '')
520 if UploadWidget.is_image(value):
521 br = BR()
522 image = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
523
524 requires = attr["requires"]
525 if requires == [] or isinstance(requires, IS_EMPTY_OR):
526 inp = DIV(inp, '[',
527 A(current.T(
528 UploadWidget.GENERIC_DESCRIPTION), _href=url),
529 '|',
530 INPUT(_type='checkbox',
531 _name=field.name + cls.ID_DELETE_SUFFIX,
532 _id=field.name + cls.ID_DELETE_SUFFIX),
533 LABEL(current.T(cls.DELETE_FILE),
534 _for=field.name + cls.ID_DELETE_SUFFIX),
535 ']', br, image)
536 else:
537 inp = DIV(inp, '[',
538 A(cls.GENERIC_DESCRIPTION, _href=url),
539 ']', br, image)
540 return inp
541
542 @classmethod
544 """
545 how to represent the file:
546
547 - with download url and if it is an image: <A href=...><IMG ...></A>
548 - otherwise with download url: <A href=...>file</A>
549 - otherwise: file
550
551 :param field: the field
552 :param value: the field value
553 :param download_url: url for the file download (default = None)
554 """
555
556 inp = cls.GENERIC_DESCRIPTION
557
558 if download_url and value:
559 if callable(download_url):
560 url = download_url(value)
561 else:
562 url = download_url + '/' + value
563 if cls.is_image(value):
564 inp = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
565 inp = A(inp, _href=url)
566
567 return inp
568
569 @staticmethod
571 """
572 Tries to check if the filename provided references to an image
573
574 Checking is based on filename extension. Currently recognized:
575 gif, png, jp(e)g, bmp
576
577 :param value: filename
578 """
579
580 extension = value.split('.')[-1].lower()
581 if extension in ['gif', 'png', 'jpg', 'jpeg', 'bmp']:
582 return True
583 return False
584
587 _class = 'string'
588
589 - def __init__(self, request, field, id_field=None, db=None,
590 orderby=None, limitby=(0, 10), distinct=False,
591 keyword='_autocomplete_%(tablename)s_%(fieldname)s',
592 min_length=2, help_fields=None, help_string=None):
593
594 self.help_fields = help_fields or []
595 self.help_string = help_string
596 if self.help_fields and not self.help_string:
597 self.help_string = ' '.join('%%(%s)s' for f in self.help_fields)
598
599 self.request = request
600 self.keyword = keyword % dict(tablename=field.tablename,
601 fieldname=field.name)
602 self.db = db or field._db
603 self.orderby = orderby
604 self.limitby = limitby
605 self.distinct = distinct
606 self.min_length = min_length
607 self.fields = [field]
608 if id_field:
609 self.is_reference = True
610 self.fields.append(id_field)
611 else:
612 self.is_reference = False
613 if hasattr(request, 'application'):
614 self.url = URL(args=request.args)
615 self.callback()
616 else:
617 self.url = request
618
620 if self.keyword in self.request.vars:
621 field = self.fields[0]
622 if is_gae:
623 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)
624 else:
625 rows = self.db(field.like(self.request.vars[self.keyword] + '%')).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *self.fields)
626 if rows:
627 if self.is_reference:
628 id_field = self.fields[1]
629 if self.help_fields:
630 options = [OPTION(
631 self.help_string % dict([(h.name, s[h.name]) for h in self.fields[:1] + self.help_fields]),
632 _value=s[id_field.name], _selected=(k == 0)) for k, s in enumerate(rows)]
633 else:
634 options = [OPTION(
635 s[field.name], _value=s[id_field.name],
636 _selected=(k == 0)) for k, s in enumerate(rows)]
637 raise HTTP(
638 200, SELECT(_id=self.keyword, _class='autocomplete',
639 _size=len(rows), _multiple=(len(rows) == 1),
640 *options).xml())
641 else:
642 raise HTTP(
643 200, SELECT(_id=self.keyword, _class='autocomplete',
644 _size=len(rows), _multiple=(len(rows) == 1),
645 *[OPTION(s[field.name],
646 _selected=(k == 0))
647 for k, s in enumerate(rows)]).xml())
648 else:
649 raise HTTP(200, '')
650
652 default = dict(
653 _type='text',
654 value=(not value is None and str(value)) or '',
655 )
656 attr = StringWidget._attributes(field, default, **attributes)
657 div_id = self.keyword + '_div'
658 attr['_autocomplete'] = 'off'
659 if self.is_reference:
660 key2 = self.keyword + '_aux'
661 key3 = self.keyword + '_auto'
662 attr['_class'] = 'string'
663 name = attr['_name']
664 if 'requires' in attr:
665 del attr['requires']
666 attr['_name'] = key2
667 value = attr['value']
668 record = self.db(
669 self.fields[1] == value).select(self.fields[0]).first()
670 attr['value'] = record and record[self.fields[0].name]
671 attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
672 dict(div_id=div_id, u='F' + self.keyword)
673 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');" % \
674 dict(url=self.url, min_length=self.min_length,
675 key=self.keyword, id=attr['_id'], key2=key2, key3=key3,
676 name=name, div_id=div_id, u='F' + self.keyword)
677 if self.min_length == 0:
678 attr['_onfocus'] = attr['_onkeyup']
679 return TAG[''](INPUT(**attr), INPUT(_type='hidden', _id=key3, _value=value,
680 _name=name, requires=field.requires),
681 DIV(_id=div_id, _style='position:absolute;'))
682 else:
683 attr['_name'] = field.name
684 attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
685 dict(div_id=div_id, u='F' + self.keyword)
686 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');" % \
687 dict(url=self.url, min_length=self.min_length,
688 key=self.keyword, id=attr['_id'], div_id=div_id, u='F' + self.keyword)
689 if self.min_length == 0:
690 attr['_onfocus'] = attr['_onkeyup']
691 return TAG[''](INPUT(**attr), DIV(_id=div_id, _style='position:absolute;'))
692
695 ''' 3 column table - default '''
696 table = TABLE()
697 for id, label, controls, help in fields:
698 _help = TD(help, _class='w2p_fc')
699 _controls = TD(controls, _class='w2p_fw')
700 _label = TD(label, _class='w2p_fl')
701 table.append(TR(_label, _controls, _help, _id=id))
702 return table
703
706 ''' 2 column table '''
707 table = TABLE()
708 for id, label, controls, help in fields:
709 _help = TD(help, _class='w2p_fc', _width='50%')
710 _controls = TD(controls, _class='w2p_fw', _colspan='2')
711 _label = TD(label, _class='w2p_fl', _width='50%')
712 table.append(TR(_label, _help, _id=id + '1', _class='even'))
713 table.append(TR(_controls, _id=id + '2', _class='odd'))
714 return table
715
718 ''' divs only '''
719 table = TAG['']()
720 for id, label, controls, help in fields:
721 _help = DIV(help, _class='w2p_fc')
722 _controls = DIV(controls, _class='w2p_fw')
723 _label = DIV(label, _class='w2p_fl')
724 table.append(DIV(_label, _controls, _help, _id=id))
725 return table
726
729 ''' divs only '''
730 if len(fields) != 2:
731 raise RuntimeError("Not possible")
732 id, label, controls, help = fields[0]
733 submit_button = fields[1][2]
734 return CAT(DIV(controls, _style='display:inline'),
735 submit_button)
736
739 ''' unordered list '''
740 table = UL()
741 for id, label, controls, help in fields:
742 _help = DIV(help, _class='w2p_fc')
743 _controls = DIV(controls, _class='w2p_fw')
744 _label = DIV(label, _class='w2p_fl')
745 table.append(LI(_label, _controls, _help, _id=id))
746 return table
747
750 ''' bootstrap format form layout '''
751 form['_class'] = 'form-horizontal'
752 parent = FIELDSET()
753 for id, label, controls, help in fields:
754 # wrappers
755 _help = SPAN(help, _class='help-inline')
756 # embed _help into _controls
757 _controls = DIV(controls, _help, _class='controls')
758 # submit unflag by default
759 _submit = False
760
761 if isinstance(controls, INPUT):
762 controls.add_class('input-xlarge')
763 if controls['_type'] == 'submit':
764 # flag submit button
765 _submit = True
766 controls['_class'] = 'btn btn-primary'
767 if controls['_type'] == 'file':
768 controls['_class'] = 'input-file'
769
770 # For password fields, which are wrapped in a CAT object.
771 if isinstance(controls, CAT) and isinstance(controls[0], INPUT):
772 controls[0].add_class('input-xlarge')
773
774 if isinstance(controls, SELECT):
775 controls.add_class('input-xlarge')
776
777 if isinstance(controls, TEXTAREA):
778 controls.add_class('input-xlarge')
779
780 if isinstance(label, LABEL):
781 label['_class'] = 'control-label'
782
783 if _submit:
784 # submit button has unwrapped label and controls, different class
785 parent.append(DIV(label, controls, _class='form-actions'))
786 # unflag submit (possible side effect)
787 _submit = False
788 else:
789 # unwrapped label
790 parent.append(DIV(label, _controls, _class='control-group'))
791 return parent
792
795
796 """
797 SQLFORM is used to map a table (and a current record) into an HTML form
798
799 given a SQLTable stored in db.table
800
801 generates an insert form::
802
803 SQLFORM(db.table)
804
805 generates an update form::
806
807 record=db.table[some_id]
808 SQLFORM(db.table, record)
809
810 generates an update with a delete button::
811
812 SQLFORM(db.table, record, deletable=True)
813
814 if record is an int::
815
816 record=db.table[record]
817
818 optional arguments:
819
820 :param fields: a list of fields that should be placed in the form,
821 default is all.
822 :param labels: a dictionary with labels for each field, keys are the field
823 names.
824 :param col3: a dictionary with content for an optional third column
825 (right of each field). keys are field names.
826 :param linkto: the URL of a controller/function to access referencedby
827 records
828 see controller appadmin.py for examples
829 :param upload: the URL of a controller/function to download an uploaded file
830 see controller appadmin.py for examples
831
832 any named optional attribute is passed to the <form> tag
833 for example _class, _id, _style, _action, _method, etc.
834
835 """
836
837 # usability improvements proposal by fpp - 4 May 2008 :
838 # - correct labels (for points to field id, not field name)
839 # - add label for delete checkbox
840 # - add translatable label for record ID
841 # - add third column to right of fields, populated from the col3 dict
842
843 widgets = Storage(dict(
844 string=StringWidget,
845 text=TextWidget,
846 password=PasswordWidget,
847 integer=IntegerWidget,
848 double=DoubleWidget,
849 decimal=DecimalWidget,
850 time=TimeWidget,
851 date=DateWidget,
852 datetime=DatetimeWidget,
853 upload=UploadWidget,
854 boolean=BooleanWidget,
855 blob=None,
856 options=OptionsWidget,
857 multiple=MultipleOptionsWidget,
858 radio=RadioWidget,
859 checkboxes=CheckboxesWidget,
860 autocomplete=AutocompleteWidget,
861 list=ListWidget,
862 ))
863
864 formstyles = Storage(dict(
865 table3cols=formstyle_table3cols,
866 table2cols=formstyle_table2cols,
867 divs=formstyle_divs,
868 ul=formstyle_ul,
869 bootstrap=formstyle_bootstrap,
870 inline=formstyle_inline,
871 ))
872
873 FIELDNAME_REQUEST_DELETE = 'delete_this_record'
874 FIELDKEY_DELETE_RECORD = 'delete_record'
875 ID_LABEL_SUFFIX = '__label'
876 ID_ROW_SUFFIX = '__row'
877
879 if not status and self.record and self.errors:
880 ### if there are errors in update mode
881 # and some errors refers to an already uploaded file
882 # delete error if
883 # - user not trying to upload a new file
884 # - there is existing file and user is not trying to delete it
885 # this is because removing the file may not pass validation
886 for key in self.errors.keys():
887 if key in self.table \
888 and self.table[key].type == 'upload' \
889 and request_vars.get(key, None) in (None, '') \
890 and self.record[key] \
891 and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars:
892 del self.errors[key]
893 if not self.errors:
894 status = True
895 return status
896
897 - def __init__(
898 self,
899 table,
900 record=None,
901 deletable=False,
902 linkto=None,
903 upload=None,
904 fields=None,
905 labels=None,
906 col3={},
907 submit_button='Submit',
908 delete_label='Check to delete',
909 showid=True,
910 readonly=False,
911 comments=True,
912 keepopts=[],
913 ignore_rw=False,
914 record_id=None,
915 formstyle='table3cols',
916 buttons=['submit'],
917 separator=': ',
918 **attributes
919 ):
920 """
921 SQLFORM(db.table,
922 record=None,
923 fields=['name'],
924 labels={'name': 'Your name'},
925 linkto=URL(f='table/db/')
926 """
927 T = current.T
928
929 self.ignore_rw = ignore_rw
930 self.formstyle = formstyle
931 self.readonly = readonly
932 # Default dbio setting
933 self.detect_record_change = None
934
935 nbsp = XML(' ') # Firefox2 does not display fields with blanks
936 FORM.__init__(self, *[], **attributes)
937 ofields = fields
938 keyed = hasattr(table, '_primarykey') # for backward compatibility
939
940 # if no fields are provided, build it from the provided table
941 # will only use writable or readable fields, unless forced to ignore
942 if fields is None:
943 fields = [f.name for f in table if
944 (ignore_rw or f.writable or f.readable) and
945 (readonly or not f.compute)]
946 self.fields = fields
947
948 # make sure we have an id
949 if self.fields[0] != table.fields[0] and \
950 isinstance(table, Table) and not keyed:
951 self.fields.insert(0, table.fields[0])
952
953 self.table = table
954
955 # try to retrieve the indicated record using its id
956 # otherwise ignore it
957 if record and isinstance(record, (int, long, str, unicode)):
958 if not str(record).isdigit():
959 raise HTTP(404, "Object not found")
960 record = table._db(table._id == record).select().first()
961 if not record:
962 raise HTTP(404, "Object not found")
963 self.record = record
964
965 self.record_id = record_id
966 if keyed:
967 self.record_id = dict([(k, record and str(record[k]) or None)
968 for k in table._primarykey])
969 self.field_parent = {}
970 xfields = []
971 self.fields = fields
972 self.custom = Storage()
973 self.custom.dspval = Storage()
974 self.custom.inpval = Storage()
975 self.custom.label = Storage()
976 self.custom.comment = Storage()
977 self.custom.widget = Storage()
978 self.custom.linkto = Storage()
979
980 # default id field name
981 if not keyed:
982 self.id_field_name = table._id.name
983 else:
984 self.id_field_name = table._primarykey[0] # only works if one key
985
986 sep = separator or ''
987
988 for fieldname in self.fields:
989 if fieldname.find('.') >= 0:
990 continue
991
992 field = self.table[fieldname]
993 comment = None
994
995 if comments:
996 comment = col3.get(fieldname, field.comment)
997 if comment is None:
998 comment = ''
999 self.custom.comment[fieldname] = comment
1000
1001 if not labels is None and fieldname in labels:
1002 label = labels[fieldname]
1003 else:
1004 label = field.label
1005 self.custom.label[fieldname] = label
1006
1007 field_id = '%s_%s' % (table._tablename, fieldname)
1008
1009 label = LABEL(label, label and sep, _for=field_id,
1010 _id=field_id + SQLFORM.ID_LABEL_SUFFIX)
1011
1012 row_id = field_id + SQLFORM.ID_ROW_SUFFIX
1013 if field.type == 'id':
1014 self.custom.dspval.id = nbsp
1015 self.custom.inpval.id = ''
1016 widget = ''
1017
1018 # store the id field name (for legacy databases)
1019 self.id_field_name = field.name
1020
1021 if record:
1022 if showid and field.name in record and field.readable:
1023 v = record[field.name]
1024 widget = SPAN(v, _id=field_id)
1025 self.custom.dspval.id = str(v)
1026 xfields.append((row_id, label, widget, comment))
1027 self.record_id = str(record[field.name])
1028 self.custom.widget.id = widget
1029 continue
1030
1031 if readonly and not ignore_rw and not field.readable:
1032 continue
1033
1034 if record:
1035 default = record[fieldname]
1036 else:
1037 default = field.default
1038 if isinstance(default, CALLABLETYPES):
1039 default = default()
1040
1041 cond = readonly or \
1042 (not ignore_rw and not field.writable and field.readable)
1043
1044 if default and not cond:
1045 default = field.formatter(default)
1046 dspval = default
1047 inpval = default
1048
1049 if cond:
1050
1051 # ## if field.represent is available else
1052 # ## ignore blob and preview uploaded images
1053 # ## format everything else
1054
1055 if field.represent:
1056 inp = represent(field, default, record)
1057 elif field.type in ['blob']:
1058 continue
1059 elif field.type == 'upload':
1060 inp = UploadWidget.represent(field, default, upload)
1061 elif field.type == 'boolean':
1062 inp = self.widgets.boolean.widget(
1063 field, default, _disabled=True)
1064 else:
1065 inp = field.formatter(default)
1066 elif field.type == 'upload':
1067 if field.widget:
1068 inp = field.widget(field, default, upload)
1069 else:
1070 inp = self.widgets.upload.widget(field, default, upload)
1071 elif field.widget:
1072 inp = field.widget(field, default)
1073 elif field.type == 'boolean':
1074 inp = self.widgets.boolean.widget(field, default)
1075 if default:
1076 inpval = 'checked'
1077 else:
1078 inpval = ''
1079 elif OptionsWidget.has_options(field):
1080 if not field.requires.multiple:
1081 inp = self.widgets.options.widget(field, default)
1082 else:
1083 inp = self.widgets.multiple.widget(field, default)
1084 if fieldname in keepopts:
1085 inpval = TAG[''](*inp.components)
1086 elif field.type.startswith('list:'):
1087 inp = self.widgets.list.widget(field, default)
1088 elif field.type == 'text':
1089 inp = self.widgets.text.widget(field, default)
1090 elif field.type == 'password':
1091 inp = self.widgets.password.widget(field, default)
1092 if self.record:
1093 dspval = PasswordWidget.DEFAULT_PASSWORD_DISPLAY
1094 else:
1095 dspval = ''
1096 elif field.type == 'blob':
1097 continue
1098 else:
1099 field_type = widget_class.match(str(field.type)).group()
1100 field_type = field_type in self.widgets and field_type or 'string'
1101 inp = self.widgets[field_type].widget(field, default)
1102
1103 xfields.append((row_id, label, inp, comment))
1104 self.custom.dspval[fieldname] = dspval or nbsp
1105 self.custom.inpval[
1106 fieldname] = inpval if not inpval is None else ''
1107 self.custom.widget[fieldname] = inp
1108
1109 # if a record is provided and found, as is linkto
1110 # build a link
1111 if record and linkto:
1112 db = linkto.split('/')[-1]
1113 for rfld in table._referenced_by:
1114 if keyed:
1115 query = urllib.quote('%s.%s==%s' % (
1116 db, rfld, record[rfld.type[10:].split('.')[1]]))
1117 else:
1118 query = urllib.quote(
1119 '%s.%s==%s' % (db, rfld, record[self.id_field_name]))
1120 lname = olname = '%s.%s' % (rfld.tablename, rfld.name)
1121 if ofields and not olname in ofields:
1122 continue
1123 if labels and lname in labels:
1124 lname = labels[lname]
1125 widget = A(lname,
1126 _class='reference',
1127 _href='%s/%s?query=%s' % (linkto, rfld.tablename, query))
1128 xfields.append(
1129 (olname.replace('.', '__') + SQLFORM.ID_ROW_SUFFIX,
1130 '', widget, col3.get(olname, '')))
1131 self.custom.linkto[olname.replace('.', '__')] = widget
1132 # </block>
1133
1134 # when deletable, add delete? checkbox
1135 self.custom.deletable = ''
1136 if record and deletable:
1137 #add secondary css class for cascade delete warning
1138 css = 'delete'
1139 for f in self.table.fields:
1140 on_del = self.table[f].ondelete
1141 if isinstance(on_del,str) and 'cascade' in on_del.lower():
1142 css += ' cascade_delete'
1143 break
1144 widget = INPUT(_type='checkbox',
1145 _class=css,
1146 _id=self.FIELDKEY_DELETE_RECORD,
1147 _name=self.FIELDNAME_REQUEST_DELETE,
1148 )
1149 xfields.append(
1150 (self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_ROW_SUFFIX,
1151 LABEL(
1152 delete_label, separator,
1153 _for=self.FIELDKEY_DELETE_RECORD,
1154 _id=self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_LABEL_SUFFIX),
1155 widget,
1156 col3.get(self.FIELDKEY_DELETE_RECORD, '')))
1157 self.custom.deletable = widget
1158
1159 # when writable, add submit button
1160 self.custom.submit = ''
1161 if not readonly:
1162 if 'submit' in buttons:
1163 widget = self.custom.submit = INPUT(_type='submit',
1164 _value=T(submit_button))
1165 elif buttons:
1166 widget = self.custom.submit = DIV(*buttons)
1167 if self.custom.submit:
1168 xfields.append(('submit_record' + SQLFORM.ID_ROW_SUFFIX,
1169 '', widget, col3.get('submit_button', '')))
1170
1171 # if a record is provided and found
1172 # make sure it's id is stored in the form
1173 if record:
1174 if not self['hidden']:
1175 self['hidden'] = {}
1176 if not keyed:
1177 self['hidden']['id'] = record[table._id.name]
1178
1179 (begin, end) = self._xml()
1180 self.custom.begin = XML("<%s %s>" % (self.tag, begin))
1181 self.custom.end = XML("%s</%s>" % (end, self.tag))
1182 table = self.createform(xfields)
1183 self.components = [table]
1184
1186 formstyle = self.formstyle
1187 if isinstance(formstyle, basestring):
1188 if formstyle in SQLFORM.formstyles:
1189 formstyle = SQLFORM.formstyles[formstyle]
1190 else:
1191 raise RuntimeError('formstyle not found')
1192
1193 if callable(formstyle):
1194 # backward compatibility, 4 argument function is the old style
1195 args, varargs, keywords, defaults = inspect.getargspec(formstyle)
1196 if defaults and len(args) - len(defaults) == 4 or len(args) == 4:
1197 table = TABLE()
1198 for id, a, b, c in xfields:
1199 newrows = formstyle(id, a, b, c)
1200 self.field_parent[id] = getattr(b, 'parent', None)
1201 if type(newrows).__name__ != "tuple":
1202 newrows = [newrows]
1203 for newrow in newrows:
1204 table.append(newrow)
1205 else:
1206 table = formstyle(self, xfields)
1207 for id, a, b, c in xfields:
1208 self.field_parent[id] = getattr(b, 'parent', None)
1209 else:
1210 raise RuntimeError('formstyle not supported')
1211 return table
1212
1213 - def accepts(
1214 self,
1215 request_vars,
1216 session=None,
1217 formname='%(tablename)s/%(record_id)s',
1218 keepvalues=None,
1219 onvalidation=None,
1220 dbio=True,
1221 hideerror=False,
1222 detect_record_change=False,
1223 **kwargs
1224 ):
1225
1226 """
1227 similar FORM.accepts but also does insert, update or delete in DAL.
1228 but if detect_record_change == True than:
1229 form.record_changed = False (record is properly validated/submitted)
1230 form.record_changed = True (record cannot be submitted because changed)
1231 elseif detect_record_change == False than:
1232 form.record_changed = None
1233 """
1234
1235 if keepvalues is None:
1236 keepvalues = True if self.record else False
1237
1238 if self.readonly:
1239 return False
1240
1241 if request_vars.__class__.__name__ == 'Request':
1242 request_vars = request_vars.post_vars
1243
1244 keyed = hasattr(self.table, '_primarykey')
1245
1246 # implement logic to detect whether record exist but has been modified
1247 # server side
1248 self.record_changed = None
1249 self.detect_record_change = detect_record_change
1250 if self.detect_record_change:
1251 if self.record:
1252 self.record_changed = False
1253 serialized = '|'.join(
1254 str(self.record[k]) for k in self.table.fields())
1255 self.record_hash = md5_hash(serialized)
1256
1257 # logic to deal with record_id for keyed tables
1258 if self.record:
1259 if keyed:
1260 formname_id = '.'.join(str(self.record[k])
1261 for k in self.table._primarykey
1262 if hasattr(self.record, k))
1263 record_id = dict((k, request_vars.get(k, None))
1264 for k in self.table._primarykey)
1265 else:
1266 (formname_id, record_id) = (self.record[self.id_field_name],
1267 request_vars.get('id', None))
1268 keepvalues = True
1269 else:
1270 if keyed:
1271 formname_id = 'create'
1272 record_id = dict([(k, None) for k in self.table._primarykey])
1273 else:
1274 (formname_id, record_id) = ('create', None)
1275
1276 if not keyed and isinstance(record_id, (list, tuple)):
1277 record_id = record_id[0]
1278
1279 if formname:
1280 formname = formname % dict(tablename=self.table._tablename,
1281 record_id=formname_id)
1282
1283 # ## THIS IS FOR UNIQUE RECORDS, read IS_NOT_IN_DB
1284
1285 for fieldname in self.fields:
1286 field = self.table[fieldname]
1287 requires = field.requires or []
1288 if not isinstance(requires, (list, tuple)):
1289 requires = [requires]
1290 [item.set_self_id(self.record_id) for item in requires
1291 if hasattr(item, 'set_self_id') and self.record_id]
1292
1293 # ## END
1294
1295 fields = {}
1296 for key in self.vars:
1297 fields[key] = self.vars[key]
1298
1299 ret = FORM.accepts(
1300 self,
1301 request_vars,
1302 session,
1303 formname,
1304 keepvalues,
1305 onvalidation,
1306 hideerror=hideerror,
1307 **kwargs
1308 )
1309
1310 self.deleted = \
1311 request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
1312
1313 self.custom.end = TAG[''](self.hidden_fields(), self.custom.end)
1314
1315 auch = record_id and self.errors and self.deleted
1316
1317 if self.record_changed and self.detect_record_change:
1318 message_onchange = \
1319 kwargs.setdefault("message_onchange",
1320 current.T("A record change was detected. " +
1321 "Consecutive update self-submissions " +
1322 "are not allowed. Try re-submitting or " +
1323 "refreshing the form page."))
1324 if message_onchange is not None:
1325 current.response.flash = message_onchange
1326 return ret
1327 elif (not ret) and (not auch):
1328 # auch is true when user tries to delete a record
1329 # that does not pass validation, yet it should be deleted
1330 for fieldname in self.fields:
1331 field = self.table[fieldname]
1332 ### this is a workaround! widgets should always have default not None!
1333 if not field.widget and field.type.startswith('list:') and \
1334 not OptionsWidget.has_options(field):
1335 field.widget = self.widgets.list.widget
1336 if field.widget and fieldname in request_vars:
1337 if fieldname in self.request_vars:
1338 value = self.request_vars[fieldname]
1339 elif self.record:
1340 value = self.record[fieldname]
1341 else:
1342 value = self.table[fieldname].default
1343 if field.type.startswith('list:') and isinstance(value, str):
1344 value = [value]
1345 row_id = '%s_%s%s' % (
1346 self.table, fieldname, SQLFORM.ID_ROW_SUFFIX)
1347 widget = field.widget(field, value)
1348 parent = self.field_parent[row_id]
1349 if parent:
1350 parent.components = [widget]
1351 if self.errors.get(fieldname):
1352 parent._traverse(False, hideerror)
1353 self.custom.widget[fieldname] = widget
1354 self.accepted = ret
1355 return ret
1356
1357 if record_id and str(record_id) != str(self.record_id):
1358 raise SyntaxError('user is tampering with form\'s record_id: '
1359 '%s != %s' % (record_id, self.record_id))
1360
1361 if record_id and dbio and not keyed:
1362 self.vars.id = self.record[self.id_field_name]
1363
1364 if self.deleted and self.custom.deletable:
1365 if dbio:
1366 if keyed:
1367 qry = reduce(lambda x, y: x & y,
1368 [self.table[k] == record_id[k]
1369 for k in self.table._primarykey])
1370 else:
1371 qry = self.table._id == self.record[self.id_field_name]
1372 self.table._db(qry).delete()
1373 self.errors.clear()
1374 for component in self.elements('input, select, textarea'):
1375 component['_disabled'] = True
1376 self.accepted = True
1377 return True
1378
1379 for fieldname in self.fields:
1380 if not fieldname in self.table.fields:
1381 continue
1382
1383 if not self.ignore_rw and not self.table[fieldname].writable:
1384 ### this happens because FORM has no knowledge of writable
1385 ### and thinks that a missing boolean field is a None
1386 if self.table[fieldname].type == 'boolean' and \
1387 self.vars.get(fieldname, True) is None:
1388 del self.vars[fieldname]
1389 continue
1390
1391 field = self.table[fieldname]
1392 if field.type == 'id':
1393 continue
1394 if field.type == 'boolean':
1395 if self.vars.get(fieldname, False):
1396 self.vars[fieldname] = fields[fieldname] = True
1397 else:
1398 self.vars[fieldname] = fields[fieldname] = False
1399 elif field.type == 'password' and self.record\
1400 and request_vars.get(fieldname, None) == \
1401 PasswordWidget.DEFAULT_PASSWORD_DISPLAY:
1402 continue # do not update if password was not changed
1403 elif field.type == 'upload':
1404 f = self.vars[fieldname]
1405 fd = '%s__delete' % fieldname
1406 if f == '' or f is None:
1407 if self.vars.get(fd, False):
1408 f = self.table[fieldname].default or ''
1409 fields[fieldname] = f
1410 elif self.record:
1411 if self.record[fieldname]:
1412 fields[fieldname] = self.record[fieldname]
1413 else:
1414 f = self.table[fieldname].default or ''
1415 fields[fieldname] = f
1416 else:
1417 fields[fieldname] = ''
1418 self.vars[fieldname] = fields[fieldname]
1419 if not f:
1420 continue
1421 else:
1422 f = os.path.join(current.request.folder,
1423 os.path.normpath(f))
1424 source_file = open(f, 'rb')
1425 original_filename = os.path.split(f)[1]
1426 elif hasattr(f, 'file'):
1427 (source_file, original_filename) = (f.file, f.filename)
1428 elif isinstance(f, (str, unicode)):
1429 ### do not know why this happens, it should not
1430 (source_file, original_filename) = \
1431 (cStringIO.StringIO(f), 'file.txt')
1432 newfilename = field.store(source_file, original_filename,
1433 field.uploadfolder)
1434 # this line was for backward compatibility but problematic
1435 # self.vars['%s_newfilename' % fieldname] = newfilename
1436 fields[fieldname] = newfilename
1437 if isinstance(field.uploadfield, str):
1438 fields[field.uploadfield] = source_file.read()
1439 # proposed by Hamdy (accept?) do we need fields at this point?
1440 self.vars[fieldname] = fields[fieldname]
1441 continue
1442 elif fieldname in self.vars:
1443 fields[fieldname] = self.vars[fieldname]
1444 elif field.default is None and field.type != 'blob':
1445 self.errors[fieldname] = 'no data'
1446 self.accepted = False
1447 return False
1448 value = fields.get(fieldname, None)
1449 if field.type == 'list:string':
1450 if not isinstance(value, (tuple, list)):
1451 fields[fieldname] = value and [value] or []
1452 elif isinstance(field.type, str) and field.type.startswith('list:'):
1453 if not isinstance(value, list):
1454 fields[fieldname] = [safe_int(
1455 x) for x in (value and [value] or [])]
1456 elif field.type == 'integer':
1457 if not value is None:
1458 fields[fieldname] = safe_int(value)
1459 elif field.type.startswith('reference'):
1460 if not value is None and isinstance(self.table, Table) and not keyed:
1461 fields[fieldname] = safe_int(value)
1462 elif field.type == 'double':
1463 if not value is None:
1464 fields[fieldname] = safe_float(value)
1465
1466 for fieldname in self.vars:
1467 if fieldname != 'id' and fieldname in self.table.fields\
1468 and not fieldname in fields and not fieldname\
1469 in request_vars:
1470 fields[fieldname] = self.vars[fieldname]
1471
1472 if dbio:
1473 if 'delete_this_record' in fields:
1474 # this should never happen but seems to happen to some
1475 del fields['delete_this_record']
1476 for field in self.table:
1477 if not field.name in fields and field.writable is False \
1478 and field.update is None and field.compute is None:
1479 if record_id and self.record:
1480 fields[field.name] = self.record[field.name]
1481 elif not self.table[field.name].default is None:
1482 fields[field.name] = self.table[field.name].default
1483 if keyed:
1484 if reduce(lambda x, y: x and y, record_id.values()): # if record_id
1485 if fields:
1486 qry = reduce(lambda x, y: x & y,
1487 [self.table[k] == self.record[k] for k in self.table._primarykey])
1488 self.table._db(qry).update(**fields)
1489 else:
1490 pk = self.table.insert(**fields)
1491 if pk:
1492 self.vars.update(pk)
1493 else:
1494 ret = False
1495 else:
1496 if record_id:
1497 self.vars.id = self.record[self.id_field_name]
1498 if fields:
1499 self.table._db(self.table._id == self.record[
1500 self.id_field_name]).update(**fields)
1501 else:
1502 self.vars.id = self.table.insert(**fields)
1503 self.accepted = ret
1504 return ret
1505
1506 AUTOTYPES = {
1507 type(''): ('string', None),
1508 type(True): ('boolean', None),
1509 type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
1510 type(1.0): ('double', IS_FLOAT_IN_RANGE()),
1511 type([]): ('list:string', None),
1512 type(datetime.date.today()): ('date', IS_DATE()),
1513 type(datetime.datetime.today()): ('datetime', IS_DATETIME())
1514 }
1515
1516 @staticmethod
1518 fields = []
1519 for key, value in sorted(dictionary.items()):
1520 t, requires = SQLFORM.AUTOTYPES.get(type(value), (None, None))
1521 if t:
1522 fields.append(Field(key, t, requires=requires,
1523 default=value))
1524 return SQLFORM.factory(*fields, **kwargs)
1525
1526 @staticmethod
1528 import os
1529 if query:
1530 session[name] = query.db(query).select().first().as_dict()
1531 elif os.path.exists(filename):
1532 env = {'datetime': datetime}
1533 session[name] = eval(open(filename).read(), {}, env)
1534 form = SQLFORM.dictform(session[name])
1535 if form.process().accepted:
1536 session[name].update(form.vars)
1537 if query:
1538 query.db(query).update(**form.vars)
1539 else:
1540 open(filename, 'w').write(repr(session[name]))
1541 return form
1542
1543 @staticmethod
1545 """
1546 generates a SQLFORM for the given fields.
1547
1548 Internally will build a non-database based data model
1549 to hold the fields.
1550 """
1551 # Define a table name, this way it can be logical to our CSS.
1552 # And if you switch from using SQLFORM to SQLFORM.factory
1553 # your same css definitions will still apply.
1554
1555 table_name = attributes.get('table_name', 'no_table')
1556
1557 # So it won't interfear with SQLDB.define_table
1558 if 'table_name' in attributes:
1559 del attributes['table_name']
1560
1561 return SQLFORM(DAL(None).define_table(table_name, *fields),
1562 **attributes)
1563
1564 @staticmethod
1566 request = current.request
1567 if isinstance(keywords, (tuple, list)):
1568 keywords = keywords[0]
1569 request.vars.keywords = keywords
1570 key = keywords.strip()
1571 if key and not ' ' in key and not '"' in key and not "'" in key:
1572 SEARCHABLE_TYPES = ('string', 'text', 'list:string')
1573 parts = [field.contains(
1574 key) for field in fields if field.type in SEARCHABLE_TYPES]
1575 else:
1576 parts = None
1577 if parts:
1578 return reduce(lambda a, b: a | b, parts)
1579 else:
1580 return smart_query(fields, key)
1581
1582 @staticmethod
1675
1676 @staticmethod
1677 - def grid(query,
1678 fields=None,
1679 field_id=None,
1680 left=None,
1681 headers={},
1682 orderby=None,
1683 groupby=None,
1684 searchable=True,
1685 sortable=True,
1686 paginate=20,
1687 deletable=True,
1688 editable=True,
1689 details=True,
1690 selectable=None,
1691 create=True,
1692 csv=True,
1693 links=None,
1694 links_in_grid=True,
1695 upload='<default>',
1696 args=[],
1697 user_signature=True,
1698 maxtextlengths={},
1699 maxtextlength=20,
1700 onvalidation=None,
1701 onfailure=None,
1702 oncreate=None,
1703 onupdate=None,
1704 ondelete=None,
1705 sorter_icons=(XML('↑'), XML('↓')),
1706 ui = 'web2py',
1707 showbuttontext=True,
1708 _class="web2py_grid",
1709 formname='web2py_grid',
1710 search_widget='default',
1711 ignore_rw = False,
1712 formstyle = 'table3cols',
1713 exportclasses = None,
1714 formargs={},
1715 createargs={},
1716 editargs={},
1717 viewargs={},
1718 buttons_placement = 'right',
1719 links_placement = 'right'
1720 ):
1721
1722 # jQuery UI ThemeRoller classes (empty if ui is disabled)
1723 if ui == 'jquery-ui':
1724 ui = dict(widget='ui-widget',
1725 header='ui-widget-header',
1726 content='ui-widget-content',
1727 default='ui-state-default',
1728 cornerall='ui-corner-all',
1729 cornertop='ui-corner-top',
1730 cornerbottom='ui-corner-bottom',
1731 button='ui-button-text-icon-primary',
1732 buttontext='ui-button-text',
1733 buttonadd='ui-icon ui-icon-plusthick',
1734 buttonback='ui-icon ui-icon-arrowreturnthick-1-w',
1735 buttonexport='ui-icon ui-icon-transferthick-e-w',
1736 buttondelete='ui-icon ui-icon-trash',
1737 buttonedit='ui-icon ui-icon-pencil',
1738 buttontable='ui-icon ui-icon-triangle-1-e',
1739 buttonview='ui-icon ui-icon-zoomin',
1740 )
1741 elif ui == 'web2py':
1742 ui = dict(widget='',
1743 header='',
1744 content='',
1745 default='',
1746 cornerall='',
1747 cornertop='',
1748 cornerbottom='',
1749 button='button btn',
1750 buttontext='buttontext button',
1751 buttonadd='icon plus icon-plus',
1752 buttonback='icon leftarrow icon-arrow-left',
1753 buttonexport='icon downarrow icon-download',
1754 buttondelete='icon trash icon-trash',
1755 buttonedit='icon pen icon-pencil',
1756 buttontable='icon rightarrow icon-arrow-right',
1757 buttonview='icon magnifier icon-zoom-in',
1758 )
1759 elif not isinstance(ui, dict):
1760 raise RuntimeError('SQLFORM.grid ui argument must be a dictionary')
1761
1762 db = query._db
1763 T = current.T
1764 request = current.request
1765 session = current.session
1766 response = current.response
1767 logged = session.auth and session.auth.user
1768 wenabled = (not user_signature or logged)
1769 create = wenabled and create
1770 editable = wenabled and editable
1771 deletable = wenabled and deletable
1772
1773 def url(**b):
1774 b['args'] = args + b.get('args', [])
1775 localvars = request.vars.copy()
1776 localvars.update(b.get('vars', {}))
1777 b['vars'] = localvars
1778 b['hash_vars'] = False
1779 b['user_signature'] = user_signature
1780 return URL(**b)
1781
1782 def url2(**b):
1783 b['args'] = request.args + b.get('args', [])
1784 localvars = request.vars.copy()
1785 localvars.update(b.get('vars', {}))
1786 b['vars'] = localvars
1787 b['hash_vars'] = False
1788 b['user_signature'] = user_signature
1789 return URL(**b)
1790
1791 referrer = session.get('_web2py_grid_referrer_' + formname, url())
1792 # if not user_signature every action is accessible
1793 # else forbid access unless
1794 # - url is based url
1795 # - url has valid signature (vars are not signed, only path_info)
1796 # = url does not contain 'create','delete','edit' (readonly)
1797 if user_signature:
1798 if not (
1799 '/'.join(str(a) for a in args) == '/'.join(request.args) or
1800 URL.verify(request,user_signature=user_signature,
1801 hash_vars=False) or
1802 (request.args(len(args))=='view' and not logged)):
1803 session.flash = T('not authorized')
1804 redirect(referrer)
1805
1806 def gridbutton(buttonclass='buttonadd', buttontext=T('Add'),
1807 buttonurl=url(args=[]), callback=None,
1808 delete=None, trap=True):
1809 if showbuttontext:
1810 if callback:
1811 return A(SPAN(_class=ui.get(buttonclass)),
1812 SPAN(T(buttontext), _title=buttontext,
1813 _class=ui.get('buttontext')),
1814 callback=callback, delete=delete,
1815 _class=trap_class(ui.get('button'), trap))
1816 else:
1817 return A(SPAN(_class=ui.get(buttonclass)),
1818 SPAN(T(buttontext), _title=buttontext,
1819 _class=ui.get('buttontext')),
1820 _href=buttonurl,
1821 _class=trap_class(ui.get('button'), trap))
1822 else:
1823 if callback:
1824 return A(SPAN(_class=ui.get(buttonclass)),
1825 callback=callback, delete=delete,
1826 _title=buttontext,
1827 _class=trap_class(ui.get('buttontext'), trap))
1828 else:
1829 return A(SPAN(_class=ui.get(buttonclass)),
1830 _href=buttonurl, _title=buttontext,
1831 _class=trap_class(ui.get('buttontext'), trap))
1832 dbset = db(query)
1833 tablenames = db._adapter.tables(dbset.query)
1834 if left is not None:
1835 tablenames += db._adapter.tables(left)
1836 tables = [db[tablename] for tablename in tablenames]
1837 if not fields:
1838 fields = reduce(lambda a, b: a + b,
1839 [[field for field in table] for table in tables])
1840 if not field_id:
1841 field_id = tables[0]._id
1842 columns = [str(field) for field in fields
1843 if field._tablename in tablenames]
1844
1845 if not any(str(f)==str(field_id) for f in fields):
1846 fields = [f for f in fields]+[field_id]
1847 table = field_id.table
1848 tablename = table._tablename
1849 if upload == '<default>':
1850 upload = lambda filename: url(args=['download', filename])
1851 if len(request.args) > 1 and request.args[-2] == 'download':
1852 stream = response.download(request, db)
1853 raise HTTP(200, stream, **response.headers)
1854
1855 def buttons(edit=False, view=False, record=None):
1856 buttons = DIV(gridbutton('buttonback', 'Back', referrer),
1857 _class='form_header row_buttons %(header)s %(cornertop)s' % ui)
1858 if edit and (not callable(edit) or edit(record)):
1859 args = ['edit', table._tablename, request.args[-1]]
1860 buttons.append(gridbutton('buttonedit', 'Edit',
1861 url(args=args)))
1862 if view:
1863 args = ['view', table._tablename, request.args[-1]]
1864 buttons.append(gridbutton('buttonview', 'View',
1865 url(args=args)))
1866 if record and links:
1867 for link in links:
1868 if isinstance(link, dict):
1869 buttons.append(link['body'](record))
1870 elif link(record):
1871 buttons.append(link(record))
1872 return buttons
1873
1874 def linsert(lst, i, x):
1875 """
1876 a = [1,2]
1877 linsert(a, 1, [0,3])
1878 a = [1, 0, 3, 2]
1879 """
1880 lst[i:i] = x
1881
1882 formfooter = DIV(
1883 _class='form_footer row_buttons %(header)s %(cornerbottom)s' % ui)
1884
1885 create_form = update_form = view_form = search_form = None
1886 sqlformargs = dict(formargs)
1887
1888 if create and len(request.args) > 1 and request.args[-2] == 'new':
1889 table = db[request.args[-1]]
1890 sqlformargs.update(createargs)
1891 create_form = SQLFORM(
1892 table, ignore_rw=ignore_rw, formstyle=formstyle,
1893 _class='web2py_form',
1894 **sqlformargs)
1895 create_form.process(formname=formname,
1896 next=referrer,
1897 onvalidation=onvalidation,
1898 onfailure=onfailure,
1899 onsuccess=oncreate)
1900 res = DIV(buttons(), create_form, formfooter, _class=_class)
1901 res.create_form = create_form
1902 res.update_form = update_form
1903 res.view_form = view_form
1904 res.search_form = search_form
1905 return res
1906
1907 elif details and len(request.args) > 2 and request.args[-3] == 'view':
1908 table = db[request.args[-2]]
1909 record = table(request.args[-1]) or redirect(referrer)
1910 sqlformargs.update(viewargs)
1911 view_form = SQLFORM(
1912 table, record, upload=upload, ignore_rw=ignore_rw,
1913 formstyle=formstyle, readonly=True, _class='web2py_form',
1914 **sqlformargs)
1915 res = DIV(buttons(edit=editable, record=record), view_form,
1916 formfooter, _class=_class)
1917 res.create_form = create_form
1918 res.update_form = update_form
1919 res.view_form = view_form
1920 res.search_form = search_form
1921 return res
1922 elif editable and len(request.args) > 2 and request.args[-3] == 'edit':
1923 table = db[request.args[-2]]
1924 record = table(request.args[-1]) or redirect(URL('error'))
1925 sqlformargs.update(editargs)
1926 update_form = SQLFORM(
1927 table,
1928 record, upload=upload, ignore_rw=ignore_rw,
1929 formstyle=formstyle, deletable=deletable,
1930 _class='web2py_form',
1931 submit_button=T('Submit'),
1932 delete_label=T('Check to delete'),
1933 **sqlformargs)
1934 update_form.process(
1935 formname=formname,
1936 onvalidation=onvalidation,
1937 onfailure=onfailure,
1938 onsuccess=onupdate,
1939 next=referrer)
1940 res = DIV(buttons(view=details, record=record),
1941 update_form, formfooter, _class=_class)
1942 res.create_form = create_form
1943 res.update_form = update_form
1944 res.view_form = view_form
1945 res.search_form = search_form
1946 return res
1947 elif deletable and len(request.args) > 2 and request.args[-3] == 'delete':
1948 table = db[request.args[-2]]
1949 if ondelete:
1950 ondelete(table, request.args[-1])
1951 ret = db(table[table._id.name] == request.args[-1]).delete()
1952 return ret
1953
1954 exportManager = dict(
1955 csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'),
1956 csv=(ExporterCSV, 'CSV'),
1957 xml=(ExporterXML, 'XML'),
1958 html=(ExporterHTML, 'HTML'),
1959 tsv_with_hidden_cols=
1960 (ExporterTSV, 'TSV (Excel compatible, hidden cols)'),
1961 tsv=(ExporterTSV, 'TSV (Excel compatible)'))
1962 if not exportclasses is None:
1963 """
1964 remember: allow to set exportclasses=dict(csv=False) to disable the csv format
1965 """
1966 exportManager.update(exportclasses)
1967
1968 export_type = request.vars._export_type
1969 if export_type:
1970 order = request.vars.order or ''
1971 if sortable:
1972 if order and not order == 'None':
1973 if order[:1] == '~':
1974 sign, rorder = '~', order[1:]
1975 else:
1976 sign, rorder = '', order
1977 tablename, fieldname = rorder.split('.', 1)
1978 orderby = db[tablename][fieldname]
1979 if sign == '~':
1980 orderby = ~orderby
1981
1982 expcolumns = columns
1983 if export_type.endswith('with_hidden_cols'):
1984 expcolumns = [f for f in fields if f._tablename in tablenames]
1985 if export_type in exportManager and exportManager[export_type]:
1986 if request.vars.keywords:
1987 try:
1988 dbset = dbset(SQLFORM.build_query(
1989 fields, request.vars.get('keywords', '')))
1990 rows = dbset.select(cacheable=True, *expcolumns)
1991 except Exception, e:
1992 response.flash = T('Internal Error')
1993 rows = []
1994 else:
1995 rows = dbset.select(left=left, orderby=orderby,
1996 cacheable=True, *expcolumns)
1997
1998 value = exportManager[export_type]
1999 clazz = value[0] if hasattr(value, '__getitem__') else value
2000 oExp = clazz(rows)
2001 filename = '.'.join(('rows', oExp.file_ext))
2002 response.headers['Content-Type'] = oExp.content_type
2003 response.headers['Content-Disposition'] = \
2004 'attachment;filename=' + filename + ';'
2005 raise HTTP(200, oExp.export(), **response.headers)
2006
2007 elif request.vars.records and not isinstance(
2008 request.vars.records, list):
2009 request.vars.records = [request.vars.records]
2010 elif not request.vars.records:
2011 request.vars.records = []
2012
2013 session['_web2py_grid_referrer_' + formname] = url2(vars=request.vars)
2014 console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui)
2015 error = None
2016 if create:
2017 add = gridbutton(
2018 buttonclass='buttonadd',
2019 buttontext=T('Add'),
2020 buttonurl=url(args=['new', tablename]))
2021 if not searchable:
2022 console.append(add)
2023 else:
2024 add = ''
2025
2026 if searchable:
2027 sfields = reduce(lambda a, b: a + b,
2028 [[f for f in t if f.readable] for t in tables])
2029 if isinstance(search_widget, dict):
2030 search_widget = search_widget[tablename]
2031 if search_widget == 'default':
2032 prefix = formname == 'web2py_grid' and 'w2p' or 'w2p_%s' % formname
2033 search_menu = SQLFORM.search_menu(sfields, prefix=prefix)
2034 spanel_id = '%s_query_fields' % prefix
2035 sfields_id = '%s_query_panel' % prefix
2036 skeywords_id = '%s_keywords' % prefix
2037 search_widget = lambda sfield, url: CAT(FORM(
2038 INPUT(_name='keywords', _value=request.vars.keywords,
2039 _id=skeywords_id,
2040 _onfocus="jQuery('#%s').change();jQuery('#%s').slideDown();" % (spanel_id, sfields_id)),
2041 INPUT(_type='submit', _value=T('Search'), _class="btn"),
2042 INPUT(_type='submit', _value=T('Clear'), _class="btn",
2043 _onclick="jQuery('#%s').val('');" % skeywords_id),
2044 _method="GET", _action=url), search_menu)
2045 form = search_widget and search_widget(sfields, url()) or ''
2046 console.append(add)
2047 console.append(form)
2048 keywords = request.vars.get('keywords', '')
2049 try:
2050 if callable(searchable):
2051 subquery = searchable(sfields, keywords)
2052 else:
2053 subquery = SQLFORM.build_query(sfields, keywords)
2054 except RuntimeError:
2055 subquery = None
2056 error = T('Invalid query')
2057 else:
2058 subquery = None
2059
2060 if subquery:
2061 dbset = dbset(subquery)
2062 try:
2063 if left or groupby:
2064 c = 'count(*)'
2065 nrows = dbset.select(c, left=left, cacheable=True,
2066 groupby=groupby).first()[c]
2067 elif dbset._db._adapter.dbengine=='google:datastore':
2068 #if we don't set a limit, this can timeout for a large table
2069 nrows = dbset.db._adapter.count(dbset.query, limit=1000)
2070 else:
2071 nrows = dbset.count()
2072 except:
2073 nrows = 0
2074 error = T('Unsupported query')
2075
2076 order = request.vars.order or ''
2077 if sortable:
2078 if order and not order == 'None':
2079 tablename, fieldname = order.split('~')[-1].split('.', 1)
2080 sort_field = db[tablename][fieldname]
2081 exception = sort_field.type in ('date', 'datetime', 'time')
2082 if exception:
2083 orderby = (order[:1] == '~' and sort_field) or ~sort_field
2084 else:
2085 orderby = (order[:1] == '~' and ~sort_field) or sort_field
2086
2087 headcols = []
2088 if selectable:
2089 headcols.append(TH(_class=ui.get('default')))
2090 for field in fields:
2091 if columns and not str(field) in columns:
2092 continue
2093 if not field.readable:
2094 continue
2095 key = str(field)
2096 header = headers.get(str(field), field.label or key)
2097 if sortable:
2098 if key == order:
2099 key, marker = '~' + order, sorter_icons[0]
2100 elif key == order[1:]:
2101 marker = sorter_icons[1]
2102 else:
2103 marker = ''
2104 header = A(header, marker, _href=url(vars=dict(
2105 keywords=request.vars.keywords or '',
2106 order=key)), _class=trap_class())
2107 headcols.append(TH(header, _class=ui.get('default')))
2108
2109 toadd = []
2110 if links and links_in_grid:
2111 for link in links:
2112 if isinstance(link, dict):
2113 toadd.append(TH(link['header'], _class=ui.get('default')))
2114 if links_placement in ['right', 'both']:
2115 headcols.extend(toadd)
2116 if links_placement in ['left', 'both']:
2117 linsert(headcols, 0, toadd)
2118
2119 # Include extra column for buttons if needed.
2120 include_buttons_column = (details or editable or deletable or
2121 (links and links_in_grid and
2122 not all([isinstance(link, dict) for link in links])))
2123 if include_buttons_column:
2124 if buttons_placement in ['right', 'both']:
2125 headcols.append(TH(_class=ui.get('default','')))
2126 if buttons_placement in ['left', 'both']:
2127 headcols.insert(0, TH(_class=ui.get('default','')))
2128
2129 head = TR(*headcols, **dict(_class=ui.get('header')))
2130
2131 cursor = True
2132 #figure out what page we are one to setup the limitby
2133 if paginate and dbset._db._adapter.dbengine=='google:datastore':
2134 cursor = request.vars.cursor or True
2135 limitby = (0, paginate)
2136 try: page = int(request.vars.page or 1)-1
2137 except ValueError: page = 0
2138 elif paginate and paginate<nrows:
2139 try: page = int(request.vars.page or 1)-1
2140 except ValueError: page = 0
2141 limitby = (paginate*page,paginate*(page+1))
2142 else:
2143 limitby = None
2144
2145 try:
2146 table_fields = [f for f in fields if f._tablename in tablenames]
2147 if dbset._db._adapter.dbengine=='google:datastore':
2148 rows = dbset.select(left=left,orderby=orderby,
2149 groupby=groupby,limitby=limitby,
2150 reusecursor=cursor,
2151 cacheable=True,*table_fields)
2152 next_cursor = dbset._db.get('_lastcursor', None)
2153 else:
2154 rows = dbset.select(left=left,orderby=orderby,
2155 groupby=groupby,limitby=limitby,
2156 cacheable=True,*table_fields)
2157 except SyntaxError:
2158 rows = None
2159 next_cursor = None
2160 error = T("Query Not Supported")
2161 except Exception, e:
2162 rows = None
2163 next_cursor = None
2164 error = T("Query Not Supported: %s")%e
2165
2166 message = error
2167 if not message and nrows:
2168 if dbset._db._adapter.dbengine=='google:datastore' and nrows>=1000:
2169 message = T('at least %(nrows)s records found') % dict(nrows=nrows)
2170 else:
2171 message = T('%(nrows)s records found') % dict(nrows=nrows)
2172 console.append(DIV(message,_class='web2py_counter'))
2173
2174 paginator = UL()
2175 if paginate and dbset._db._adapter.dbengine=='google:datastore':
2176 #this means we may have a large table with an unknown number of rows.
2177 try:
2178 page = int(request.vars.page or 1)-1
2179 except ValueError:
2180 page = 0
2181 paginator.append(LI('page %s'%(page+1)))
2182 if next_cursor:
2183 d = dict(page=page+2, cursor=next_cursor)
2184 if order: d['order']=order
2185 if request.vars.keywords: d['keywords']=request.vars.keywords
2186 paginator.append(LI(
2187 A('next',_href=url(vars=d),_class=trap_class())))
2188 elif paginate and paginate<nrows:
2189 npages, reminder = divmod(nrows, paginate)
2190 if reminder:
2191 npages += 1
2192 try:
2193 page = int(request.vars.page or 1) - 1
2194 except ValueError:
2195 page = 0
2196
2197 def self_link(name, p):
2198 d = dict(page=p + 1)
2199 if order:
2200 d['order'] = order
2201 if request.vars.keywords:
2202 d['keywords'] = request.vars.keywords
2203 return A(name, _href=url(vars=d), _class=trap_class())
2204 NPAGES = 5 # window is 2*NPAGES
2205 if page > NPAGES + 1:
2206 paginator.append(LI(self_link('<<', 0)))
2207 if page > NPAGES:
2208 paginator.append(LI(self_link('<', page - 1)))
2209 pages = range(max(0, page - NPAGES), min(page + NPAGES, npages))
2210 for p in pages:
2211 if p == page:
2212 paginator.append(LI(A(p + 1, _onclick='return false'),
2213 _class=trap_class('current')))
2214 else:
2215 paginator.append(LI(self_link(p + 1, p)))
2216 if page < npages - NPAGES:
2217 paginator.append(LI(self_link('>', page + 1)))
2218 if page < npages - NPAGES - 1:
2219 paginator.append(LI(self_link('>>', npages - 1)))
2220 else:
2221 limitby = None
2222
2223 if rows:
2224 htmltable = TABLE(THEAD(head))
2225 tbody = TBODY()
2226 numrec = 0
2227 for row in rows:
2228 trcols = []
2229 id = row[field_id]
2230 if selectable:
2231 trcols.append(
2232 INPUT(_type="checkbox", _name="records", _value=id,
2233 value=request.vars.records))
2234 for field in fields:
2235 if not str(field) in columns:
2236 continue
2237 if not field.readable:
2238 continue
2239 if field.type == 'blob':
2240 continue
2241 value = row[field]
2242 maxlength = maxtextlengths.get(str(field), maxtextlength)
2243 if field.represent:
2244 try:
2245 value = field.represent(value, row)
2246 except KeyError:
2247 try:
2248 value = field.represent(
2249 value, row[field._tablename])
2250 except KeyError:
2251 pass
2252 elif field.type == 'boolean':
2253 value = INPUT(_type="checkbox", _checked=value,
2254 _disabled=True)
2255 elif field.type == 'upload':
2256 if value:
2257 if callable(upload):
2258 value = A(
2259 current.T('file'), _href=upload(value))
2260 elif upload:
2261 value = A(current.T('file'),
2262 _href='%s/%s' % (upload, value))
2263 else:
2264 value = ''
2265 if isinstance(value, str):
2266 value = truncate_string(value, maxlength)
2267 elif not isinstance(value, DIV):
2268 value = field.formatter(value)
2269 trcols.append(TD(value))
2270 row_buttons = TD(_class='row_buttons')
2271 if links and links_in_grid:
2272 toadd = []
2273 for link in links:
2274 if isinstance(link, dict):
2275 toadd.append(TD(link['body'](row)))
2276 else:
2277 if link(row):
2278 row_buttons.append(link(row))
2279 if links_placement in ['right', 'both']:
2280 trcols.extend(toadd)
2281 if links_placement in ['left', 'both']:
2282 linsert(trcols, 0, toadd)
2283
2284 if include_buttons_column:
2285 if details and (not callable(details) or details(row)):
2286 row_buttons.append(gridbutton(
2287 'buttonview', 'View',
2288 url(args=['view', tablename, id])))
2289 if editable and (not callable(editable) or editable(row)):
2290 row_buttons.append(gridbutton(
2291 'buttonedit', 'Edit',
2292 url(args=['edit', tablename, id])))
2293 if deletable and (not callable(deletable) or deletable(row)):
2294 row_buttons.append(gridbutton(
2295 'buttondelete', 'Delete',
2296 callback=url(args=['delete', tablename, id]),
2297 delete='tr'))
2298 if buttons_placement in ['right', 'both']:
2299 trcols.append(row_buttons)
2300 if buttons_placement in ['left', 'both']:
2301 trcols.insert(0, row_buttons)
2302 if numrec % 2 == 0:
2303 classtr = 'even'
2304 else:
2305 classtr = 'odd'
2306 numrec += 1
2307 if id:
2308 rid = id
2309 if callable(rid): # can this ever be callable?
2310 rid = rid(row)
2311 tr = TR(*trcols, **dict(
2312 _id=rid,
2313 _class='%s %s' % (classtr, 'with_id')))
2314 else:
2315 tr = TR(*trcols, **dict(_class=classtr))
2316 tbody.append(tr)
2317 htmltable.append(tbody)
2318 htmltable = DIV(htmltable, _style='width:100%;overflow-x:auto')
2319 if selectable:
2320 htmltable = FORM(htmltable, INPUT(_type="submit"))
2321 if htmltable.process(formname=formname).accepted:
2322 htmltable.vars.records = htmltable.vars.records or []
2323 htmltable.vars.records = htmltable.vars.records if type(htmltable.vars.records) == list else [htmltable.vars.records]
2324 records = [int(r) for r in htmltable.vars.records]
2325 selectable(records)
2326 redirect(referrer)
2327 else:
2328 htmltable = DIV(current.T('No records found'))
2329
2330 if csv and nrows:
2331 export_links = []
2332 for k, v in sorted(exportManager.items()):
2333 if not v:
2334 continue
2335 label = v[1] if hasattr(v, "__getitem__") else k
2336 link = url2(vars=dict(
2337 order=request.vars.order or '',
2338 _export_type=k,
2339 keywords=request.vars.keywords or ''))
2340 export_links.append(A(T(label), _href=link))
2341 export_menu = \
2342 DIV(T('Export:'), _class="w2p_export_menu", *export_links)
2343 else:
2344 export_menu = None
2345
2346 res = DIV(console, DIV(htmltable, _class="web2py_table"),
2347 _class='%s %s' % (_class, ui.get('widget')))
2348 if paginator.components:
2349 res.append(
2350 DIV(paginator,
2351 _class="web2py_paginator %(header)s %(cornerbottom)s" % ui))
2352 if export_menu:
2353 res.append(export_menu)
2354 res.create_form = create_form
2355 res.update_form = update_form
2356 res.view_form = view_form
2357 res.search_form = search_form
2358 return res
2359
2360 @staticmethod
2361 - def smartgrid(table, constraints=None, linked_tables=None,
2362 links=None, links_in_grid=True,
2363 args=None, user_signature=True,
2364 divider='>', breadcrumbs_class='',
2365 **kwargs):
2366 """
2367 @auth.requires_login()
2368 def index():
2369 db.define_table('person',Field('name'),format='%(name)s')
2370 db.define_table('dog',
2371 Field('name'),Field('owner',db.person),format='%(name)s')
2372 db.define_table('comment',Field('body'),Field('dog',db.dog))
2373 if db(db.person).isempty():
2374 from gluon.contrib.populate import populate
2375 populate(db.person,300)
2376 populate(db.dog,300)
2377 populate(db.comment,1000)
2378 db.commit()
2379 form=SQLFORM.smartgrid(db[request.args(0) or 'person']) #***
2380 return dict(form=form)
2381
2382 *** builds a complete interface to navigate all tables links
2383 to the request.args(0)
2384 table: pagination, search, view, edit, delete,
2385 children, parent, etc.
2386
2387 constraints is a dict {'table':query} that limits which
2388 records can be accessible
2389 links is a dict like
2390 {'tablename':[lambda row: A(....), ...]}
2391 that will add buttons when table tablename is displayed
2392 linked_tables is a optional list of tablenames of tables
2393 to be linked
2394 """
2395 request, T = current.request, current.T
2396 if args is None:
2397 args = []
2398
2399 def url(**b):
2400 b['args'] = request.args[:nargs] + b.get('args', [])
2401 b['hash_vars'] = False
2402 b['user_signature'] = user_signature
2403 return URL(**b)
2404
2405 db = table._db
2406 breadcrumbs = []
2407 if request.args(len(args)) != table._tablename:
2408 request.args[:] = args + [table._tablename]
2409 if links is None:
2410 links = {}
2411 if constraints is None:
2412 constraints = {}
2413 field = None
2414 try:
2415 nargs = len(args) + 1
2416 previous_tablename, previous_fieldname, previous_id = \
2417 table._tablename, None, None
2418 while len(request.args) > nargs:
2419 key = request.args(nargs)
2420 if '.' in key:
2421 id = request.args(nargs + 1)
2422 tablename, fieldname = key.split('.', 1)
2423 table = db[tablename]
2424 field = table[fieldname]
2425 field.default = id
2426 referee = field.type[10:]
2427 if referee != previous_tablename:
2428 raise HTTP(400)
2429 cond = constraints.get(referee, None)
2430 if cond:
2431 record = db(
2432 db[referee].id == id)(cond).select().first()
2433 else:
2434 record = db[referee](id)
2435 if previous_id:
2436 if record[previous_fieldname] != int(previous_id):
2437 raise HTTP(400)
2438 previous_tablename, previous_fieldname, previous_id = \
2439 tablename, fieldname, id
2440 try:
2441 format = db[referee]._format
2442 if callable(format):
2443 name = format(record)
2444 else:
2445 name = format % record
2446 except TypeError:
2447 name = id
2448 breadcrumbs.append(
2449 LI(A(T(db[referee]._plural),
2450 _class=trap_class(),
2451 _href=url()),
2452 SPAN(divider, _class='divider'),
2453 _class='w2p_grid_breadcrumb_elem'))
2454 if kwargs.get('details', True):
2455 breadcrumbs.append(
2456 LI(A(name, _class=trap_class(),
2457 _href=url(args=['view', referee, id])),
2458 SPAN(divider, _class='divider'),
2459 _class='w2p_grid_breadcrumb_elem'))
2460 nargs += 2
2461 else:
2462 break
2463 if nargs > len(args) + 1:
2464 query = (field == id)
2465 if isinstance(linked_tables, dict):
2466 linked_tables = linked_tables.get(table._tablename, [])
2467 if linked_tables is None or referee in linked_tables:
2468 field.represent = lambda id, r=None, referee=referee, rep=field.represent: A(callable(rep) and rep(id) or id, _class=trap_class(), _href=url(args=['view', referee, id]))
2469 except (KeyError, ValueError, TypeError):
2470 redirect(URL(args=table._tablename))
2471 if nargs == len(args) + 1:
2472 query = table.id > 0
2473
2474 # filter out data info for displayed table
2475 if table._tablename in constraints:
2476 query = query & constraints[table._tablename]
2477 if isinstance(links, dict):
2478 links = links.get(table._tablename, [])
2479 for key in 'columns,orderby,searchable,sortable,paginate,deletable,editable,details,selectable,create,fields'.split(','):
2480 if isinstance(kwargs.get(key, None), dict):
2481 if table._tablename in kwargs[key]:
2482 kwargs[key] = kwargs[key][table._tablename]
2483 else:
2484 del kwargs[key]
2485 check = {}
2486 id_field_name = table._id.name
2487 for rfield in table._referenced_by:
2488 check[rfield.tablename] = \
2489 check.get(rfield.tablename, []) + [rfield.name]
2490 if isinstance(linked_tables, dict):
2491 linked_tables = linked_tables.get(table._tablename, [])
2492 for tablename in sorted(check):
2493 linked_fieldnames = check[tablename]
2494 tb = db[tablename]
2495 multiple_links = len(linked_fieldnames) > 1
2496 for fieldname in linked_fieldnames:
2497 if linked_tables is None or tablename in linked_tables:
2498 t = T(tb._plural) if not multiple_links else \
2499 T(tb._plural + '(' + fieldname + ')')
2500 args0 = tablename + '.' + fieldname
2501 links.append(
2502 lambda row, t=t, nargs=nargs, args0=args0:
2503 A(SPAN(t), _class=trap_class(), _href=url(
2504 args=[args0, row[id_field_name]])))
2505
2506 grid = SQLFORM.grid(query, args=request.args[:nargs], links=links,
2507 links_in_grid=links_in_grid,
2508 user_signature=user_signature, **kwargs)
2509 if isinstance(grid, DIV):
2510 header = table._plural + (field and ' for ' + field.name or '')
2511 breadcrumbs.append(LI(A(T(header), _class=trap_class(),
2512 _href=url()), _class='active w2p_grid_breadcrumb_elem'))
2513 grid.insert(
2514 0, DIV(UL(*breadcrumbs, **{'_class': breadcrumbs_class}),
2515 _class='web2py_breadcrumbs'))
2516 return grid
2517
2520
2521 """
2522 given a Rows object, as returned by a db().select(), generates
2523 an html table with the rows.
2524
2525 optional arguments:
2526
2527 :param linkto: URL (or lambda to generate a URL) to edit individual records
2528 :param upload: URL to download uploaded files
2529 :param orderby: Add an orderby link to column headers.
2530 :param headers: dictionary of headers to headers redefinions
2531 headers can also be a string to gerenare the headers from data
2532 for now only headers="fieldname:capitalize",
2533 headers="labels" and headers=None are supported
2534 :param truncate: length at which to truncate text in table cells.
2535 Defaults to 16 characters.
2536 :param columns: a list or dict contaning the names of the columns to be shown
2537 Defaults to all
2538
2539 Optional names attributes for passed to the <table> tag
2540
2541 The keys of headers and columns must be of the form "tablename.fieldname"
2542
2543 Simple linkto example::
2544
2545 rows = db.select(db.sometable.ALL)
2546 table = SQLTABLE(rows, linkto='someurl')
2547
2548 This will link rows[id] to .../sometable/value_of_id
2549
2550
2551 More advanced linkto example::
2552
2553 def mylink(field, type, ref):
2554 return URL(args=[field])
2555
2556 rows = db.select(db.sometable.ALL)
2557 table = SQLTABLE(rows, linkto=mylink)
2558
2559 This will link rows[id] to
2560 current_app/current_controlle/current_function/value_of_id
2561
2562 New Implements: 24 June 2011:
2563 -----------------------------
2564
2565 :param selectid: The id you want to select
2566 :param renderstyle: Boolean render the style with the table
2567
2568 :param extracolums = [{'label':A('Extra',_href='#'),
2569 'class': '', #class name of the header
2570 'width':'', #width in pixels or %
2571 'content':lambda row, rc: A('Edit',_href='edit/%s'%row.id),
2572 'selected': False #agregate class selected to this column
2573 }]
2574
2575
2576 :param headers = {'table.id':{'label':'Id',
2577 'class':'', #class name of the header
2578 'width':'', #width in pixels or %
2579 'truncate': 16, #truncate the content to...
2580 'selected': False #agregate class selected to this column
2581 },
2582 'table.myfield':{'label':'My field',
2583 'class':'', #class name of the header
2584 'width':'', #width in pixels or %
2585 'truncate': 16, #truncate the content to...
2586 'selected': False #agregate class selected to this column
2587 },
2588 }
2589
2590 table = SQLTABLE(rows, headers=headers, extracolums=extracolums)
2591
2592
2593 """
2594
2595 - def __init__(
2596 self,
2597 sqlrows,
2598 linkto=None,
2599 upload=None,
2600 orderby=None,
2601 headers={},
2602 truncate=16,
2603 columns=None,
2604 th_link='',
2605 extracolumns=None,
2606 selectid=None,
2607 renderstyle=False,
2608 cid=None,
2609 **attributes
2610 ):
2611
2612 TABLE.__init__(self, **attributes)
2613
2614 self.components = []
2615 self.attributes = attributes
2616 self.sqlrows = sqlrows
2617 (components, row) = (self.components, [])
2618 if not sqlrows:
2619 return
2620 if not columns:
2621 columns = sqlrows.colnames
2622 if headers == 'fieldname:capitalize':
2623 headers = {}
2624 for c in columns:
2625 headers[c] = c.split('.')[-1].replace('_', ' ').title()
2626 elif headers == 'labels':
2627 headers = {}
2628 for c in columns:
2629 (t, f) = c.split('.')
2630 field = sqlrows.db[t][f]
2631 headers[c] = field.label
2632 if headers is None:
2633 headers = {}
2634 else:
2635 for c in columns: # new implement dict
2636 if isinstance(headers.get(c, c), dict):
2637 coldict = headers.get(c, c)
2638 attrcol = dict()
2639 if coldict['width'] != "":
2640 attrcol.update(_width=coldict['width'])
2641 if coldict['class'] != "":
2642 attrcol.update(_class=coldict['class'])
2643 row.append(TH(coldict['label'], **attrcol))
2644 elif orderby:
2645 row.append(TH(A(headers.get(c, c),
2646 _href=th_link + '?orderby=' + c, cid=cid)))
2647 else:
2648 row.append(TH(headers.get(c, c)))
2649
2650 if extracolumns: # new implement dict
2651 for c in extracolumns:
2652 attrcol = dict()
2653 if c['width'] != "":
2654 attrcol.update(_width=c['width'])
2655 if c['class'] != "":
2656 attrcol.update(_class=c['class'])
2657 row.append(TH(c['label'], **attrcol))
2658
2659 components.append(THEAD(TR(*row)))
2660
2661 tbody = []
2662 for (rc, record) in enumerate(sqlrows):
2663 row = []
2664 if rc % 2 == 0:
2665 _class = 'even'
2666 else:
2667 _class = 'odd'
2668
2669 if not selectid is None: # new implement
2670 if record.get('id') == selectid:
2671 _class += ' rowselected'
2672
2673 for colname in columns:
2674 if not table_field.match(colname):
2675 if "_extra" in record and colname in record._extra:
2676 r = record._extra[colname]
2677 row.append(TD(r))
2678 continue
2679 else:
2680 raise KeyError(
2681 "Column %s not found (SQLTABLE)" % colname)
2682 (tablename, fieldname) = colname.split('.')
2683 try:
2684 field = sqlrows.db[tablename][fieldname]
2685 except (KeyError, AttributeError):
2686 field = None
2687 if tablename in record \
2688 and isinstance(record, Row) \
2689 and isinstance(record[tablename], Row):
2690 r = record[tablename][fieldname]
2691 elif fieldname in record:
2692 r = record[fieldname]
2693 else:
2694 raise SyntaxError('something wrong in Rows object')
2695 r_old = r
2696 if not field or isinstance(field, (Field.Virtual, Field.Lazy)):
2697 pass
2698 elif linkto and field.type == 'id':
2699 try:
2700 href = linkto(r, 'table', tablename)
2701 except TypeError:
2702 href = '%s/%s/%s' % (linkto, tablename, r_old)
2703 r = A(r, _href=href)
2704 elif isinstance(field.type, str) and field.type.startswith('reference'):
2705 if linkto:
2706 ref = field.type[10:]
2707 try:
2708 href = linkto(r, 'reference', ref)
2709 except TypeError:
2710 href = '%s/%s/%s' % (linkto, ref, r_old)
2711 if ref.find('.') >= 0:
2712 tref, fref = ref.split('.')
2713 if hasattr(sqlrows.db[tref], '_primarykey'):
2714 href = '%s/%s?%s' % (linkto, tref, urllib.urlencode({fref: r}))
2715 r = A(represent(field, r, record), _href=str(href))
2716 elif field.represent:
2717 r = represent(field, r, record)
2718 elif linkto and hasattr(field._table, '_primarykey')\
2719 and fieldname in field._table._primarykey:
2720 # have to test this with multi-key tables
2721 key = urllib.urlencode(dict([
2722 ((tablename in record
2723 and isinstance(record, Row)
2724 and isinstance(record[tablename], Row)) and
2725 (k, record[tablename][k])) or (k, record[k])
2726 for k in field._table._primarykey]))
2727 r = A(r, _href='%s/%s?%s' % (linkto, tablename, key))
2728 elif isinstance(field.type, str) and field.type.startswith('list:'):
2729 r = represent(field, r or [], record)
2730 elif field.represent:
2731 r = represent(field, r, record)
2732 elif field.type == 'blob' and r:
2733 r = 'DATA'
2734 elif field.type == 'upload':
2735 if upload and r:
2736 r = A(current.T('file'), _href='%s/%s' % (upload, r))
2737 elif r:
2738 r = current.T('file')
2739 else:
2740 r = ''
2741 elif field.type in ['string', 'text']:
2742 r = str(field.formatter(r))
2743 if headers != {}: # new implement dict
2744 if isinstance(headers[colname], dict):
2745 if isinstance(headers[colname]['truncate'], int):
2746 r = truncate_string(
2747 r, headers[colname]['truncate'])
2748 elif not truncate is None:
2749 r = truncate_string(r, truncate)
2750 attrcol = dict() # new implement dict
2751 if headers != {}:
2752 if isinstance(headers[colname], dict):
2753 colclass = headers[colname]['class']
2754 if headers[colname]['selected']:
2755 colclass = str(headers[colname]
2756 ['class'] + " colselected").strip()
2757 if colclass != "":
2758 attrcol.update(_class=colclass)
2759
2760 row.append(TD(r, **attrcol))
2761
2762 if extracolumns: # new implement dict
2763 for c in extracolumns:
2764 attrcol = dict()
2765 colclass = c['class']
2766 if c['selected']:
2767 colclass = str(c['class'] + " colselected").strip()
2768 if colclass != "":
2769 attrcol.update(_class=colclass)
2770 contentfunc = c['content']
2771 row.append(TD(contentfunc(record, rc), **attrcol))
2772
2773 tbody.append(TR(_class=_class, *row))
2774
2775 if renderstyle:
2776 components.append(STYLE(self.style()))
2777
2778 components.append(TBODY(*tbody))
2779
2781
2782 css = '''
2783 table tbody tr.odd {
2784 background-color: #DFD;
2785 }
2786 table tbody tr.even {
2787 background-color: #EFE;
2788 }
2789 table tbody tr.rowselected {
2790 background-color: #FDD;
2791 }
2792 table tbody tr td.colselected {
2793 background-color: #FDD;
2794 }
2795 table tbody tr:hover {
2796 background: #DDF;
2797 }
2798 '''
2799
2800 return css
2801
2802 form_factory = SQLFORM.factory # for backward compatibility, deprecated
2806 label = None
2807 file_ext = None
2808 content_type = None
2809
2812
2814 def none_exception(value):
2815 """
2816 returns a cleaned up value that can be used for csv export:
2817 - unicode text is encoded as such
2818 - None values are replaced with the given representation (default <NULL>)
2819 """
2820 if value is None:
2821 return '<NULL>'
2822 elif isinstance(value, unicode):
2823 return value.encode('utf8')
2824 elif isinstance(value, Reference):
2825 return int(value)
2826 elif hasattr(value, 'isoformat'):
2827 return value.isoformat()[:19].replace('T', ' ')
2828 elif isinstance(value, (list, tuple)): # for type='list:..'
2829 return bar_encode(value)
2830 return value
2831
2832 represented = []
2833 for record in self.rows:
2834 row = []
2835 for col in self.rows.colnames:
2836 if not REGEX_TABLE_DOT_FIELD.match(col):
2837 row.append(record._extra[col])
2838 else:
2839 (t, f) = col.split('.')
2840 field = self.rows.db[t][f]
2841 if isinstance(record.get(t, None), (Row, dict)):
2842 value = record[t][f]
2843 else:
2844 value = record[f]
2845 if field.type == 'blob' and not value is None:
2846 value = ''
2847 elif field.represent:
2848 value = field.represent(value, record)
2849 row.append(none_exception(value))
2850
2851 represented.append(row)
2852 return represented
2853
2856
2859
2860 label = 'TSV'
2861 file_ext = "csv"
2862 content_type = "text/tab-separated-values"
2863
2866
2868
2869 out = cStringIO.StringIO()
2870 final = cStringIO.StringIO()
2871 import csv
2872 writer = csv.writer(out, delimiter='\t')
2873 if self.rows:
2874 import codecs
2875 final.write(codecs.BOM_UTF16)
2876 writer.writerow(
2877 [unicode(col).encode("utf8") for col in self.rows.colnames])
2878 data = out.getvalue().decode("utf8")
2879 data = data.encode("utf-16")
2880 data = data[2:]
2881 final.write(data)
2882 out.truncate(0)
2883 records = self.represented()
2884 for row in records:
2885 writer.writerow(
2886 [str(col).decode('utf8').encode("utf-8") for col in row])
2887 data = out.getvalue().decode("utf8")
2888 data = data.encode("utf-16")
2889 data = data[2:]
2890 final.write(data)
2891 out.truncate(0)
2892 return str(final.getvalue())
2893
2908
2911 label = 'HTML'
2912 file_ext = "html"
2913 content_type = "text/html"
2914
2917
2919 out = cStringIO.StringIO()
2920 out.write('<html>\n<body>\n<table>\n')
2921 if self.rows:
2922 colnames = [a.split('.') for a in self.rows.colnames]
2923 for row in self.rows.records:
2924 out.write('<tr>\n')
2925 for col in colnames:
2926 out.write('<td>' + str(row[col[0]][col[1]]) + '</td>\n')
2927 out.write('</tr>\n')
2928 out.write('</table>\n</body>\n</html>')
2929 return str(out.getvalue())
2930
2933 label = 'XML'
2934 file_ext = "xml"
2935 content_type = "text/xml"
2936
2939
2941 out = cStringIO.StringIO()
2942 out.write('<rows>\n')
2943 if self.rows:
2944 colnames = [a.split('.') for a in self.rows.colnames]
2945 for row in self.rows.records:
2946 out.write('<row>\n')
2947 for col in colnames:
2948 out.write(
2949 '<%s>' % col + str(row[col[0]][col[1]]) + '</%s>\n' % col)
2950 out.write('</row>\n')
2951 out.write('</rows>')
2952 return str(out.getvalue())
2953
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Dec 24 13:54:53 2012 | http://epydoc.sourceforge.net |