Package web2py :: Package gluon :: Module sqlhtml
[hide private]
[frames] | no frames]

Source Code for Module web2py.gluon.sqlhtml

   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   
46 -def trap_class(_class=None, trap=True):
47 return (trap and 'w2p_trap' or '') + (_class and ' ' + _class or '')
48 49
50 -def represent(field, value, record):
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
65 -def safe_int(x):
66 try: 67 return int(x) 68 except ValueError: 69 return 0
70 71
72 -def safe_float(x):
73 try: 74 return float(x) 75 except ValueError: 76 return 0
77 78
79 -class FormWidget(object):
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
88 - def _attributes(cls, field, 89 widget_attributes, **attributes):
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
110 - def widget(cls, field, value, **attributes):
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
128 -class StringWidget(FormWidget):
129 _class = 'string' 130 131 @classmethod
132 - def widget(cls, field, value, **attributes):
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
148 -class IntegerWidget(StringWidget):
149 _class = 'integer'
150 151
152 -class DoubleWidget(StringWidget):
153 _class = 'double'
154 155
156 -class DecimalWidget(StringWidget):
157 _class = 'decimal'
158 159
160 -class TimeWidget(StringWidget):
161 _class = 'time'
162 163
164 -class DateWidget(StringWidget):
165 _class = 'date'
166 167
168 -class DatetimeWidget(StringWidget):
169 _class = 'datetime'
170 171
172 -class TextWidget(FormWidget):
173 _class = 'text' 174 175 @classmethod
176 - def widget(cls, field, value, **attributes):
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
188 -class BooleanWidget(FormWidget):
189 _class = 'boolean' 190 191 @classmethod
192 - def widget(cls, field, value, **attributes):
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
205 -class OptionsWidget(FormWidget):
206 207 @staticmethod
208 - def has_options(field):
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
219 - def widget(cls, field, value, **attributes):
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
242 -class ListWidget(StringWidget):
243 244 @classmethod
245 - def widget(cls, field, value, **attributes):
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>&nbsp;<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
298 -class MultipleOptionsWidget(OptionsWidget):
299 300 @classmethod
301 - def widget(cls, field, value, size=5, **attributes):
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
316 -class RadioWidget(OptionsWidget):
317 318 @classmethod
319 - def widget(cls, field, value, **attributes):
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
374 -class CheckboxesWidget(OptionsWidget):
375 376 @classmethod
377 - def widget(cls, field, value, **attributes):
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
443 -class PasswordWidget(FormWidget):
444 _class = 'password' 445 446 DEFAULT_PASSWORD_DISPLAY = 8 * ('*') 447 448 @classmethod
449 - def widget(cls, field, value, **attributes):
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
478 -class UploadWidget(FormWidget):
479 _class = 'upload' 480 481 DEFAULT_WIDTH = '150px' 482 ID_DELETE_SUFFIX = '__delete' 483 GENERIC_DESCRIPTION = 'file' 484 DELETE_FILE = 'delete' 485 486 @classmethod
487 - def widget(cls, field, value, download_url=None, **attributes):
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
534 - def represent(cls, field, value, download_url=None):
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
561 - def is_image(value):
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
577 -class AutocompleteWidget(object):
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
610 - def callback(self):
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
642 - def __call__(self, field, value, **attributes):
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
685 -def formstyle_table3cols(form, fields):
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
696 -def formstyle_table2cols(form, fields):
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
708 -def formstyle_divs(form, fields):
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
719 -def formstyle_inline(form, fields):
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
729 -def formstyle_ul(form, fields):
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
740 -def formstyle_bootstrap(form, fields):
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
785 -class SQLFORM(FORM):
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
869 - def assert_status(self, status, request_vars):
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('&nbsp;') # 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
1169 - def createform(self, xfields):
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
1501 - def dictform(dictionary, **kwargs):
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
1511 - def smartdictform(session, name, filename=None, query=None, **kwargs):
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
1528 - def factory(*fields, **attributes):
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
1549 - def build_query(fields, keywords):
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
1567 - def search_menu(fields, 1568 search_options=None, 1569 prefix='w2p' 1570 ):
1571 T = current.T 1572 panel_id='%s_query_panel' % prefix 1573 fields_id='%s_query_fields' % prefix 1574 keywords_id='%s_keywords' % prefix 1575 field_id='%s_field' % prefix 1576 value_id='%s_value' % prefix 1577 search_options = search_options or { 1578 'string': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'], 1579 'text': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'], 1580 'date': ['=', '!=', '<', '>', '<=', '>='], 1581 'time': ['=', '!=', '<', '>', '<=', '>='], 1582 'datetime': ['=', '!=', '<', '>', '<=', '>='], 1583 'integer': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], 1584 'double': ['=', '!=', '<', '>', '<=', '>='], 1585 'id': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], 1586 'reference': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], 1587 'boolean': ['=', '!=']} 1588 if fields[0]._db._adapter.dbengine == 'google:datastore': 1589 search_options['string'] = ['=', '!=', '<', '>', '<=', '>='] 1590 search_options['text'] = ['=', '!=', '<', '>', '<=', '>='] 1591 search_options['list:string'] = ['contains'] 1592 search_options['list:integer'] = ['contains'] 1593 search_options['list:reference'] = ['contains'] 1594 criteria = [] 1595 selectfields = [] 1596 for field in fields: 1597 name = str(field).replace('.', '-') 1598 options = search_options.get(field.type.split(' ')[0], None) 1599 if options: 1600 label = isinstance( 1601 field.label, str) and T(field.label) or field.label 1602 selectfields.append(OPTION(label, _value=str(field))) 1603 operators = SELECT(*[OPTION(T(option), _value=option) for option in options]) 1604 if field.type == 'boolean': 1605 value_input = SELECT( 1606 OPTION(T("True"), _value="T"), 1607 OPTION(T("False"), _value="F"), 1608 _id="%s_%s" % (value_id,name)) 1609 else: 1610 value_input = INPUT(_type='text', 1611 _id="%s_%s" % (value_id,name), 1612 _class=field.type) 1613 new_button = INPUT( 1614 _type="button", _value=T('New'), _class="btn", 1615 _onclick="%s_build_query('new','%s')" % (prefix,field)) 1616 and_button = INPUT( 1617 _type="button", _value=T('And'), _class="btn", 1618 _onclick="%s_build_query('and','%s')" % (prefix, field)) 1619 or_button = INPUT( 1620 _type="button", _value=T('Or'), _class="btn", 1621 _onclick="%s_build_query('or','%s')" % (prefix, field)) 1622 close_button = INPUT( 1623 _type="button", _value=T('Close'), _class="btn", 1624 _onclick="jQuery('#%s').slideUp()" % panel_id) 1625 1626 criteria.append(DIV( 1627 operators, value_input, new_button, 1628 and_button, or_button, close_button, 1629 _id='%s_%s' % (field_id, name), 1630 _class='w2p_query_row hidden', 1631 _style='display:inline')) 1632 1633 criteria.insert(0, SELECT( 1634 _id=fields_id, 1635 _onchange="jQuery('.w2p_query_row').hide();jQuery('#%s_'+jQuery('#%s').val().replace('.','-')).show();" % (field_id,fields_id), 1636 _style='float:left', 1637 *selectfields)) 1638 1639 fadd = SCRIPT(""" 1640 jQuery('#%(fields_id)s input,#%(fields_id)s select').css( 1641 'width','auto'); 1642 jQuery(function(){web2py_ajax_fields('#%(fields_id)s');}); 1643 function %(prefix)s_build_query(aggregator,a) { 1644 var b=a.replace('.','-'); 1645 var option = jQuery('#%(field_id)s_'+b+' select').val(); 1646 var value = jQuery('#%(value_id)s_'+b).val().replace('"','\\\\"'); 1647 var s=a+' '+option+' "'+value+'"'; 1648 var k=jQuery('#%(keywords_id)s'); 1649 var v=k.val(); 1650 if(aggregator=='new') k.val(s); else k.val((v?(v+' '+ aggregator +' '):'')+s); 1651 } 1652 """ % dict( 1653 prefix=prefix,fields_id=fields_id,keywords_id=keywords_id, 1654 field_id=field_id,value_id=value_id 1655 ) 1656 ) 1657 return CAT( 1658 DIV(_id=panel_id, _class='hidden', *criteria), fadd)
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('&#x2191;'), XML('&#x2193;')), 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 = []