/gluon/sqlhtml.py
Python | 3114 lines | 3043 code | 36 blank | 35 comment | 136 complexity | 64303a9106989697b69b9f6773bcd202 MD5 | raw file
Possible License(s): BSD-2-Clause, MIT, BSD-3-Clause
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """
- This file is part of the web2py Web Framework
- Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
- License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
- Holds:
- - SQLFORM: provide a form for a table (with/without record)
- - SQLTABLE: provides a table for a set of records
- - form_factory: provides a SQLFORM for an non-db backed table
- """
- try:
- from urlparse import parse_qs as psq
- except ImportError:
- from cgi import parse_qs as psq
- import os
- import copy
- from http import HTTP
- from html import XmlComponent
- from html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT
- from html import FORM, INPUT, LABEL, OPTION, SELECT
- from html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE
- from html import URL, truncate_string, FIELDSET
- from dal import DAL, Field, Table, Row, CALLABLETYPES, smart_query, \
- bar_encode, Reference, REGEX_TABLE_DOT_FIELD
- from storage import Storage
- from utils import md5_hash
- from validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \
- IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, IS_STRONG
- import serializers
- import datetime
- import urllib
- import re
- import cStringIO
- from globals import current
- from http import redirect
- import inspect
- try:
- import settings
- is_gae = settings.global_settings.web2py_runtime_gae
- except ImportError:
- is_gae = False # this is an assumption (if settings missing)
- table_field = re.compile('[\w_]+\.[\w_]+')
- widget_class = re.compile('^\w*')
- def trap_class(_class=None, trap=True):
- return (trap and 'w2p_trap' or '') + (_class and ' ' + _class or '')
- def represent(field, value, record):
- f = field.represent
- if not callable(f):
- return str(value)
- n = f.func_code.co_argcount - len(f.func_defaults or [])
- if getattr(f, 'im_self', None):
- n -= 1
- if n == 1:
- return f(value)
- elif n == 2:
- return f(value, record)
- else:
- raise RuntimeError("field representation must take 1 or 2 args")
- def safe_int(x):
- try:
- return int(x)
- except ValueError:
- return 0
- def safe_float(x):
- try:
- return float(x)
- except ValueError:
- return 0
- def show_if(cond):
- if not cond:
- return None
- base = "%s_%s" % (cond.first.tablename, cond.first.name)
- if ((cond.op.__name__ == 'EQ' and cond.second == True) or
- (cond.op.__name__ == 'NE' and cond.second == False)):
- return base,":checked"
- if ((cond.op.__name__ == 'EQ' and cond.second == False) or
- (cond.op.__name__ == 'NE' and cond.second == True)):
- return base,":not(:checked)"
- if cond.op.__name__ == 'EQ':
- return base,"[value='%s']" % cond.second
- if cond.op.__name__ == 'NE':
- return base,"[value!='%s']" % cond.second
- if cond.op.__name__ == 'CONTAINS':
- return base,"[value~='%s']" % cond.second
- if cond.op.__name__ == 'BELONGS' and isinstance(cond.second,(list,tuple)):
- return base,','.join("[value='%s']" % (v) for v in cond.second)
- raise RuntimeError("Not Implemented Error")
- class FormWidget(object):
- """
- helper for SQLFORM to generate form input fields
- (widget), related to the fieldtype
- """
- _class = 'generic-widget'
- @classmethod
- def _attributes(cls, field,
- widget_attributes, **attributes):
- """
- helper to build a common set of attributes
- :param field: the field involved,
- some attributes are derived from this
- :param widget_attributes: widget related attributes
- :param attributes: any other supplied attributes
- """
- attr = dict(
- _id='%s_%s' % (field.tablename, field.name),
- _class=cls._class or
- widget_class.match(str(field.type)).group(),
- _name=field.name,
- requires=field.requires,
- )
- if getattr(field,'show_if',None):
- trigger, cond = show_if(field.show_if)
- attr['_data-show-trigger'] = trigger
- attr['_data-show-if'] = cond
- attr.update(widget_attributes)
- attr.update(attributes)
- return attr
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates the widget for the field.
- When serialized, will provide an INPUT tag:
- - id = tablename_fieldname
- - class = field.type
- - name = fieldname
- :param field: the field needing the widget
- :param value: value
- :param attributes: any other attributes to be applied
- """
- raise NotImplementedError
- class StringWidget(FormWidget):
- _class = 'string'
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates an INPUT text tag.
- see also: :meth:`FormWidget.widget`
- """
- default = dict(
- _type='text',
- value=(not value is None and str(value)) or '',
- )
- attr = cls._attributes(field, default, **attributes)
- return INPUT(**attr)
- class IntegerWidget(StringWidget):
- _class = 'integer'
- class DoubleWidget(StringWidget):
- _class = 'double'
- class DecimalWidget(StringWidget):
- _class = 'decimal'
- class TimeWidget(StringWidget):
- _class = 'time'
- class DateWidget(StringWidget):
- _class = 'date'
- class DatetimeWidget(StringWidget):
- _class = 'datetime'
- class TextWidget(FormWidget):
- _class = 'text'
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates a TEXTAREA tag.
- see also: :meth:`FormWidget.widget`
- """
- default = dict(value=value)
- attr = cls._attributes(field, default, **attributes)
- return TEXTAREA(**attr)
- class JSONWidget(FormWidget):
- _class = 'json'
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates a TEXTAREA for JSON notation.
- see also: :meth:`FormWidget.widget`
- """
- if not isinstance(value, basestring):
- if value is not None:
- value = serializers.json(value)
- default = dict(value=value)
- attr = cls._attributes(field, default, **attributes)
- return TEXTAREA(**attr)
- class BooleanWidget(FormWidget):
- _class = 'boolean'
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates an INPUT checkbox tag.
- see also: :meth:`FormWidget.widget`
- """
- default = dict(_type='checkbox', value=value)
- attr = cls._attributes(field, default,
- **attributes)
- return INPUT(**attr)
- class OptionsWidget(FormWidget):
- @staticmethod
- def has_options(field):
- """
- checks if the field has selectable options
- :param field: the field needing checking
- :returns: True if the field has options
- """
- return hasattr(field.requires, 'options')
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates a SELECT tag, including OPTIONs (only 1 option allowed)
- see also: :meth:`FormWidget.widget`
- """
- default = dict(value=value)
- attr = cls._attributes(field, default,
- **attributes)
- requires = field.requires
- if not isinstance(requires, (list, tuple)):
- requires = [requires]
- if requires:
- if hasattr(requires[0], 'options'):
- options = requires[0].options()
- else:
- raise SyntaxError(
- 'widget cannot determine options of %s' % field)
- opts = [OPTION(v, _value=k) for (k, v) in options]
- return SELECT(*opts, **attr)
- class ListWidget(StringWidget):
- @classmethod
- def widget(cls, field, value, **attributes):
- _id = '%s_%s' % (field.tablename, field.name)
- _name = field.name
- if field.type == 'list:integer':
- _class = 'integer'
- else:
- _class = 'string'
- requires = field.requires if isinstance(
- field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None
- nvalue = value or ['']
- items = [LI(INPUT(_id=_id, _class=_class, _name=_name,
- value=v, hideerror=k < len(nvalue) - 1,
- requires=requires),
- **attributes) for (k, v) in enumerate(nvalue)]
- attributes['_id'] = _id + '_grow_input'
- attributes['_style'] = 'list-style:none'
- attributes['_class'] = 'w2p_list'
- return TAG[''](UL(*items, **attributes))
- class MultipleOptionsWidget(OptionsWidget):
- @classmethod
- def widget(cls, field, value, size=5, **attributes):
- """
- generates a SELECT tag, including OPTIONs (multiple options allowed)
- see also: :meth:`FormWidget.widget`
- :param size: optional param (default=5) to indicate how many rows must
- be shown
- """
- attributes.update(_size=size, _multiple=True)
- return OptionsWidget.widget(field, value, **attributes)
- class RadioWidget(OptionsWidget):
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates a TABLE tag, including INPUT radios (only 1 option allowed)
- see also: :meth:`FormWidget.widget`
- """
- if isinstance(value, (list,tuple)):
- value = str(value[0])
- else:
- value = str(value)
- attr = cls._attributes(field, {}, **attributes)
- attr['_class'] = attr.get('_class', 'web2py_radiowidget')
- requires = field.requires
- if not isinstance(requires, (list, tuple)):
- requires = [requires]
- if requires:
- if hasattr(requires[0], 'options'):
- options = requires[0].options()
- else:
- raise SyntaxError('widget cannot determine options of %s'
- % field)
- options = [(k, v) for k, v in options if str(v)]
- opts = []
- cols = attributes.get('cols', 1)
- totals = len(options)
- mods = totals % cols
- rows = totals / cols
- if mods:
- rows += 1
- #widget style
- wrappers = dict(
- table=(TABLE, TR, TD),
- ul=(DIV, UL, LI),
- divs=(CAT, DIV, DIV)
- )
- parent, child, inner = wrappers[attributes.get('style', 'table')]
- for r_index in range(rows):
- tds = []
- for k, v in options[r_index * cols:(r_index + 1) * cols]:
- checked = {'_checked': 'checked'} if k == value else {}
- tds.append(inner(INPUT(_type='radio',
- _id='%s%s' % (field.name, k),
- _name=field.name,
- requires=attr.get('requires', None),
- hideerror=True, _value=k,
- value=value,
- **checked),
- LABEL(v, _for='%s%s' % (field.name, k))))
- opts.append(child(tds))
- if opts:
- opts[-1][0][0]['hideerror'] = False
- return parent(*opts, **attr)
- class CheckboxesWidget(OptionsWidget):
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates a TABLE tag, including INPUT checkboxes (multiple allowed)
- see also: :meth:`FormWidget.widget`
- """
- # was values = re.compile('[\w\-:]+').findall(str(value))
- if isinstance(value, (list, tuple)):
- values = [str(v) for v in value]
- else:
- values = [str(value)]
- attr = cls._attributes(field, {}, **attributes)
- attr['_class'] = attr.get('_class', 'web2py_checkboxeswidget')
- requires = field.requires
- if not isinstance(requires, (list, tuple)):
- requires = [requires]
- if requires and hasattr(requires[0], 'options'):
- options = requires[0].options()
- else:
- raise SyntaxError('widget cannot determine options of %s'
- % field)
- options = [(k, v) for k, v in options if k != '']
- opts = []
- cols = attributes.get('cols', 1)
- totals = len(options)
- mods = totals % cols
- rows = totals / cols
- if mods:
- rows += 1
- #widget style
- wrappers = dict(
- table=(TABLE, TR, TD),
- ul=(DIV, UL, LI),
- divs=(CAT, DIV, DIV)
- )
- parent, child, inner = wrappers[attributes.get('style', 'table')]
- for r_index in range(rows):
- tds = []
- for k, v in options[r_index * cols:(r_index + 1) * cols]:
- if k in values:
- r_value = k
- else:
- r_value = []
- tds.append(inner(INPUT(_type='checkbox',
- _id='%s%s' % (field.name, k),
- _name=field.name,
- requires=attr.get('requires', None),
- hideerror=True, _value=k,
- value=r_value),
- LABEL(v, _for='%s%s' % (field.name, k))))
- opts.append(child(tds))
- if opts:
- opts.append(
- INPUT(requires=attr.get('requires', None),
- _style="display:none;",
- _disabled="disabled",
- _name=field.name,
- hideerror=False))
- return parent(*opts, **attr)
- class PasswordWidget(FormWidget):
- _class = 'password'
- DEFAULT_PASSWORD_DISPLAY = 8 * ('*')
- @classmethod
- def widget(cls, field, value, **attributes):
- """
- generates a INPUT password tag.
- If a value is present it will be shown as a number of '*', not related
- to the length of the actual value.
- see also: :meth:`FormWidget.widget`
- """
- # detect if attached a IS_STRONG with entropy
- default = dict(
- _type='password',
- _value=(value and cls.DEFAULT_PASSWORD_DISPLAY) or '',
- )
- attr = cls._attributes(field, default, **attributes)
- # deal with entropy check!
- requires = field.requires
- if not isinstance(requires, (list, tuple)):
- requires = [requires]
- is_strong = [r for r in requires if isinstance(r, IS_STRONG)]
- if is_strong:
- attr['_data-w2p_entropy'] = is_strong[0].entropy if is_strong[0].entropy else "null"
- # end entropy check
- output = INPUT(**attr)
- return output
- class UploadWidget(FormWidget):
- _class = 'upload'
- DEFAULT_WIDTH = '150px'
- ID_DELETE_SUFFIX = '__delete'
- GENERIC_DESCRIPTION = 'file'
- DELETE_FILE = 'delete'
- @classmethod
- def widget(cls, field, value, download_url=None, **attributes):
- """
- generates a INPUT file tag.
- Optionally provides an A link to the file, including a checkbox so
- the file can be deleted.
- All is wrapped in a DIV.
- see also: :meth:`FormWidget.widget`
- :param download_url: Optional URL to link to the file (default = None)
- """
- default = dict(_type='file',)
- attr = cls._attributes(field, default, **attributes)
- inp = INPUT(**attr)
- if download_url and value:
- if callable(download_url):
- url = download_url(value)
- else:
- url = download_url + '/' + value
- (br, image) = ('', '')
- if UploadWidget.is_image(value):
- br = BR()
- image = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
- requires = attr["requires"]
- if requires == [] or isinstance(requires, IS_EMPTY_OR):
- inp = DIV(inp,
- SPAN('[',
- A(current.T(
- UploadWidget.GENERIC_DESCRIPTION), _href=url),
- '|',
- INPUT(_type='checkbox',
- _name=field.name + cls.ID_DELETE_SUFFIX,
- _id=field.name + cls.ID_DELETE_SUFFIX),
- LABEL(current.T(cls.DELETE_FILE),
- _for=field.name + cls.ID_DELETE_SUFFIX,
- _style='display:inline'),
- ']', _style='white-space:nowrap'),
- br, image)
- else:
- inp = DIV(inp,
- SPAN('[',
- A(cls.GENERIC_DESCRIPTION, _href=url),
- ']', _style='white-space:nowrap'),
- br, image)
- return inp
- @classmethod
- def represent(cls, field, value, download_url=None):
- """
- how to represent the file:
- - with download url and if it is an image: <A href=...><IMG ...></A>
- - otherwise with download url: <A href=...>file</A>
- - otherwise: file
- :param field: the field
- :param value: the field value
- :param download_url: url for the file download (default = None)
- """
- inp = cls.GENERIC_DESCRIPTION
- if download_url and value:
- if callable(download_url):
- url = download_url(value)
- else:
- url = download_url + '/' + value
- if cls.is_image(value):
- inp = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
- inp = A(inp, _href=url)
- return inp
- @staticmethod
- def is_image(value):
- """
- Tries to check if the filename provided references to an image
- Checking is based on filename extension. Currently recognized:
- gif, png, jp(e)g, bmp
- :param value: filename
- """
- extension = value.split('.')[-1].lower()
- if extension in ['gif', 'png', 'jpg', 'jpeg', 'bmp']:
- return True
- return False
- class AutocompleteWidget(object):
- _class = 'string'
- def __init__(self, request, field, id_field=None, db=None,
- orderby=None, limitby=(0, 10), distinct=False,
- keyword='_autocomplete_%(tablename)s_%(fieldname)s',
- min_length=2, help_fields=None, help_string=None):
- self.help_fields = help_fields or []
- self.help_string = help_string
- if self.help_fields and not self.help_string:
- self.help_string = ' '.join('%%(%s)s'%f.name
- for f in self.help_fields)
- self.request = request
- self.keyword = keyword % dict(tablename=field.tablename,
- fieldname=field.name)
- self.db = db or field._db
- self.orderby = orderby
- self.limitby = limitby
- self.distinct = distinct
- self.min_length = min_length
- self.fields = [field]
- if id_field:
- self.is_reference = True
- self.fields.append(id_field)
- else:
- self.is_reference = False
- if hasattr(request, 'application'):
- self.url = URL(args=request.args)
- self.callback()
- else:
- self.url = request
- def callback(self):
- if self.keyword in self.request.vars:
- field = self.fields[0]
- if is_gae:
- 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+self.help_fields))
- else:
- rows = self.db(field.like(self.request.vars[self.keyword] + '%')).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields))
- if rows:
- if self.is_reference:
- id_field = self.fields[1]
- if self.help_fields:
- options = [OPTION(
- self.help_string % dict([(h.name, s[h.name]) for h in self.fields[:1] + self.help_fields]),
- _value=s[id_field.name], _selected=(k == 0)) for k, s in enumerate(rows)]
- else:
- options = [OPTION(
- s[field.name], _value=s[id_field.name],
- _selected=(k == 0)) for k, s in enumerate(rows)]
- raise HTTP(
- 200, SELECT(_id=self.keyword, _class='autocomplete',
- _size=len(rows), _multiple=(len(rows) == 1),
- *options).xml())
- else:
- raise HTTP(
- 200, SELECT(_id=self.keyword, _class='autocomplete',
- _size=len(rows), _multiple=(len(rows) == 1),
- *[OPTION(s[field.name],
- _selected=(k == 0))
- for k, s in enumerate(rows)]).xml())
- else:
- raise HTTP(200, '')
- def __call__(self, field, value, **attributes):
- default = dict(
- _type='text',
- value=(not value is None and str(value)) or '',
- )
- attr = StringWidget._attributes(field, default, **attributes)
- div_id = self.keyword + '_div'
- attr['_autocomplete'] = 'off'
- if self.is_reference:
- key2 = self.keyword + '_aux'
- key3 = self.keyword + '_auto'
- attr['_class'] = 'string'
- name = attr['_name']
- if 'requires' in attr:
- del attr['requires']
- attr['_name'] = key2
- value = attr['value']
- record = self.db(
- self.fields[1] == value).select(self.fields[0]).first()
- attr['value'] = record and record[self.fields[0].name]
- attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
- dict(div_id=div_id, u='F' + self.keyword)
- 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');" % \
- dict(url=self.url, min_length=self.min_length,
- key=self.keyword, id=attr['_id'], key2=key2, key3=key3,
- name=name, div_id=div_id, u='F' + self.keyword)
- if self.min_length == 0:
- attr['_onfocus'] = attr['_onkeyup']
- return TAG[''](INPUT(**attr), INPUT(_type='hidden', _id=key3, _value=value,
- _name=name, requires=field.requires),
- DIV(_id=div_id, _style='position:absolute;'))
- else:
- attr['_name'] = field.name
- attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
- dict(div_id=div_id, u='F' + self.keyword)
- 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');" % \
- dict(url=self.url, min_length=self.min_length,
- key=self.keyword, id=attr['_id'], div_id=div_id, u='F' + self.keyword)
- if self.min_length == 0:
- attr['_onfocus'] = attr['_onkeyup']
- return TAG[''](INPUT(**attr), DIV(_id=div_id, _style='position:absolute;'))
- def formstyle_table3cols(form, fields):
- ''' 3 column table - default '''
- table = TABLE()
- for id, label, controls, help in fields:
- _help = TD(help, _class='w2p_fc')
- _controls = TD(controls, _class='w2p_fw')
- _label = TD(label, _class='w2p_fl')
- table.append(TR(_label, _controls, _help, _id=id))
- return table
- def formstyle_table2cols(form, fields):
- ''' 2 column table '''
- table = TABLE()
- for id, label, controls, help in fields:
- _help = TD(help, _class='w2p_fc', _width='50%')
- _controls = TD(controls, _class='w2p_fw', _colspan='2')
- _label = TD(label, _class='w2p_fl', _width='50%')
- table.append(TR(_label, _help, _id=id + '1', _class='even'))
- table.append(TR(_controls, _id=id + '2', _class='odd'))
- return table
- def formstyle_divs(form, fields):
- ''' divs only '''
- table = FIELDSET()
- for id, label, controls, help in fields:
- _help = DIV(help, _class='w2p_fc')
- _controls = DIV(controls, _class='w2p_fw')
- _label = DIV(label, _class='w2p_fl')
- table.append(DIV(_label, _controls, _help, _id=id))
- return table
- def formstyle_inline(form, fields):
- ''' divs only '''
- if len(fields) != 2:
- raise RuntimeError("Not possible")
- id, label, controls, help = fields[0]
- submit_button = fields[1][2]
- return CAT(DIV(controls, _style='display:inline'),
- submit_button)
- def formstyle_ul(form, fields):
- ''' unordered list '''
- table = UL()
- for id, label, controls, help in fields:
- _help = DIV(help, _class='w2p_fc')
- _controls = DIV(controls, _class='w2p_fw')
- _label = DIV(label, _class='w2p_fl')
- table.append(LI(_label, _controls, _help, _id=id))
- return table
- def formstyle_bootstrap(form, fields):
- ''' bootstrap format form layout '''
- form.add_class('form-horizontal')
- parent = FIELDSET()
- for id, label, controls, help in fields:
- # wrappers
- _help = SPAN(help, _class='help-block')
- # embed _help into _controls
- _controls = DIV(controls, _help, _class='controls')
- # submit unflag by default
- _submit = False
- if isinstance(controls, INPUT):
- controls.add_class('span4')
- if controls['_type'] == 'submit':
- # flag submit button
- _submit = True
- controls['_class'] = 'btn btn-primary'
- if controls['_type'] == 'file':
- controls['_class'] = 'input-file'
- # For password fields, which are wrapped in a CAT object.
- if isinstance(controls, CAT) and isinstance(controls[0], INPUT):
- controls[0].add_class('span4')
- if isinstance(controls, SELECT):
- controls.add_class('span4')
- if isinstance(controls, TEXTAREA):
- controls.add_class('span4')
- if isinstance(label, LABEL):
- label['_class'] = 'control-label'
- if _submit:
- # submit button has unwrapped label and controls, different class
- parent.append(DIV(label, controls, _class='form-actions', _id=id))
- # unflag submit (possible side effect)
- _submit = False
- else:
- # unwrapped label
- parent.append(DIV(label, _controls, _class='control-group', _id=id))
- return parent
- class SQLFORM(FORM):
- """
- SQLFORM is used to map a table (and a current record) into an HTML form
- given a SQLTable stored in db.table
- generates an insert form::
- SQLFORM(db.table)
- generates an update form::
- record=db.table[some_id]
- SQLFORM(db.table, record)
- generates an update with a delete button::
- SQLFORM(db.table, record, deletable=True)
- if record is an int::
- record=db.table[record]
- optional arguments:
- :param fields: a list of fields that should be placed in the form,
- default is all.
- :param labels: a dictionary with labels for each field, keys are the field
- names.
- :param col3: a dictionary with content for an optional third column
- (right of each field). keys are field names.
- :param linkto: the URL of a controller/function to access referencedby
- records
- see controller appadmin.py for examples
- :param upload: the URL of a controller/function to download an uploaded file
- see controller appadmin.py for examples
- any named optional attribute is passed to the <form> tag
- for example _class, _id, _style, _action, _method, etc.
- """
- # usability improvements proposal by fpp - 4 May 2008 :
- # - correct labels (for points to field id, not field name)
- # - add label for delete checkbox
- # - add translatable label for record ID
- # - add third column to right of fields, populated from the col3 dict
- widgets = Storage(dict(
- string=StringWidget,
- text=TextWidget,
- json=JSONWidget,
- password=PasswordWidget,
- integer=IntegerWidget,
- double=DoubleWidget,
- decimal=DecimalWidget,
- time=TimeWidget,
- date=DateWidget,
- datetime=DatetimeWidget,
- upload=UploadWidget,
- boolean=BooleanWidget,
- blob=None,
- options=OptionsWidget,
- multiple=MultipleOptionsWidget,
- radio=RadioWidget,
- checkboxes=CheckboxesWidget,
- autocomplete=AutocompleteWidget,
- list=ListWidget,
- ))
- formstyles = Storage(dict(
- table3cols=formstyle_table3cols,
- table2cols=formstyle_table2cols,
- divs=formstyle_divs,
- ul=formstyle_ul,
- bootstrap=formstyle_bootstrap,
- inline=formstyle_inline,
- ))
- FIELDNAME_REQUEST_DELETE = 'delete_this_record'
- FIELDKEY_DELETE_RECORD = 'delete_record'
- ID_LABEL_SUFFIX = '__label'
- ID_ROW_SUFFIX = '__row'
- def assert_status(self, status, request_vars):
- if not status and self.record and self.errors:
- ### if there are errors in update mode
- # and some errors refers to an already uploaded file
- # delete error if
- # - user not trying to upload a new file
- # - there is existing file and user is not trying to delete it
- # this is because removing the file may not pass validation
- for key in self.errors.keys():
- if key in self.table \
- and self.table[key].type == 'upload' \
- and request_vars.get(key, None) in (None, '') \
- and self.record[key] \
- and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars:
- del self.errors[key]
- if not self.errors:
- status = True
- return status
- def __init__(
- self,
- table,
- record=None,
- deletable=False,
- linkto=None,
- upload=None,
- fields=None,
- labels=None,
- col3={},
- submit_button='Submit',
- delete_label='Check to delete',
- showid=True,
- readonly=False,
- comments=True,
- keepopts=[],
- ignore_rw=False,
- record_id=None,
- formstyle='table3cols',
- buttons=['submit'],
- separator=': ',
- **attributes
- ):
- """
- SQLFORM(db.table,
- record=None,
- fields=['name'],
- labels={'name': 'Your name'},
- linkto=URL(f='table/db/')
- """
- T = current.T
- self.ignore_rw = ignore_rw
- self.formstyle = formstyle
- self.readonly = readonly
- # Default dbio setting
- self.detect_record_change = None
- nbsp = XML(' ') # Firefox2 does not display fields with blanks
- FORM.__init__(self, *[], **attributes)
- ofields = fields
- keyed = hasattr(table, '_primarykey') # for backward compatibility
- # if no fields are provided, build it from the provided table
- # will only use writable or readable fields, unless forced to ignore
- if fields is None:
- fields = [f.name for f in table if
- (ignore_rw or f.writable or f.readable) and
- (readonly or not f.compute)]
- self.fields = fields
- # make sure we have an id
- if self.fields[0] != table.fields[0] and \
- isinstance(table, Table) and not keyed:
- self.fields.insert(0, table.fields[0])
- self.table = table
- # try to retrieve the indicated record using its id
- # otherwise ignore it
- if record and isinstance(record, (int, long, str, unicode)):
- if not str(record).isdigit():
- raise HTTP(404, "Object not found")
- record = table._db(table._id == record).select().first()
- if not record:
- raise HTTP(404, "Object not found")
- self.record = record
- self.record_id = record_id
- if keyed:
- self.record_id = dict([(k, record and str(record[k]) or None)
- for k in table._primarykey])
- self.field_parent = {}
- xfields = []
- self.fields = fields
- self.custom = Storage()
- self.custom.dspval = Storage()
- self.custom.inpval = Storage()
- self.custom.label = Storage()
- self.custom.comment = Storage()
- self.custom.widget = Storage()
- self.custom.linkto = Storage()
- # default id field name
- if not keyed:
- self.id_field_name = table._id.name
- else:
- self.id_field_name = table._primarykey[0] # only works if one key
- sep = separator or ''
- for fieldname in self.fields:
- if fieldname.find('.') >= 0:
- continue
- field = self.table[fieldname]
- comment = None
- if comments:
- comment = col3.get(fieldname, field.comment)
- if comment is None:
- comment = ''
- self.custom.comment[fieldname] = comment
- if not labels is None and fieldname in labels:
- label = labels[fieldname]
- else:
- label = field.label
- self.custom.label[fieldname] = label
- field_id = '%s_%s' % (table._tablename, fieldname)
- label = LABEL(label, label and sep, _for=field_id,
- _id=field_id + SQLFORM.ID_LABEL_SUFFIX)
- row_id = field_id + SQLFORM.ID_ROW_SUFFIX
- if field.type == 'id':
- self.custom.dspval.id = nbsp
- self.custom.inpval.id = ''
- widget = ''
- # store the id field name (for legacy databases)
- self.id_field_name = field.name
- if record:
- if showid and field.name in record and field.readable:
- v = record[field.name]
- widget = SPAN(v, _id=field_id)
- self.custom.dspval.id = str(v)
- xfields.append((row_id, label, widget, comment))
- self.record_id = str(record[field.name])
- self.custom.widget.id = widget
- continue
- if readonly and not ignore_rw and not field.readable:
- continue
- if record:
- default = record[fieldname]
- else:
- default = field.default
- if isinstance(default, CALLABLETYPES):
- default = default()
- cond = readonly or \
- (not ignore_rw and not field.writable and field.readable)
- if default is not None and not cond:
- default = field.formatter(default)
- dspval = default
- inpval = default
- if cond:
- # ## if field.represent is available else
- # ## ignore blob and preview uploaded images
- # ## format everything else
- if field.represent:
- inp = represent(field, default, record)
- elif field.type in ['blob']:
- continue
- elif field.type == 'upload':
- inp = UploadWidget.represent(field, default, upload)
- elif field.type == 'boolean':
- inp = self.widgets.boolean.widget(
- field, default, _disabled=True)
- else:
- inp = field.formatter(default)
- elif field.type == 'upload':
- if field.widget:
- inp = field.widget(field, default, upload)
- else:
- inp = self.widgets.upload.widget(field, default, upload)
- elif field.widget:
- inp = field.widget(field, default)
- elif field.type == 'boolean':
- inp = self.widgets.boolean.widget(field, default)
- if default:
- inpval = 'checked'
- else:
- inpval = ''
- elif OptionsWidget.has_options(field):
- if not field.requires.multiple:
- inp = self.widgets.options.widget(field, default)
- else:
- inp = self.widgets.multiple.widget(field, default)
- if fieldname in keepopts:
- inpval = TAG[''](*inp.components)
- elif field.type.startswith('list:'):
- inp = self.widgets.list.widget(field, default)
- elif field.type == 'text':
- inp = self.widgets.text.widget(field, default)
- elif field.type == 'password':
- inp = self.widgets.password.widget(field, default)
- if self.record:
- dspval = PasswordWidget.DEFAULT_PASSWORD_DISPLAY
- else:
- dspval = ''
- elif field.type == 'blob':
- continue
- else:
- field_type = widget_class.match(str(field.type)).group()
- field_type = field_type in self.widgets and field_type or 'string'
- inp = self.widgets[field_type].widget(field, default)
- xfields.append((row_id, label, inp, comment))
- self.custom.dspval[fieldname] = dspval if (dspval is not None) else nbsp
- self.custom.inpval[
- fieldname] = inpval if not inpval is None else ''
- self.custom.widget[fieldname] = inp
- # if a record is provided and found, as is linkto
- # build a link
- if record and linkto:
- db = linkto.split('/')[-1]
- for rfld in table._referenced_by:
- if keyed:
- query = urllib.quote('%s.%s==%s' % (
- db, rfld, record[rfld.type[10:].split('.')[1]]))
- else:
- query = urllib.quote(
- '%s.%s==%s' % (db, rfld, record[self.id_field_name]))
- lname = olname = '%s.%s' % (rfld.tablename, rfld.name)
- if ofields and not olname in ofields:
- continue
- if labels and lname in labels:
- lname = labels[lname]
- widget = A(lname,
- _class='reference',
- _href='%s/%s?query=%s' % (linkto, rfld.tablename, query))
- xfields.append(
- (olname.replace('.', '__') + SQLFORM.ID_ROW_SUFFIX,
- '', widget, col3.get(olname, '')))
- self.custom.linkto[olname.replace('.', '__')] = widget
- # </block>
- # when deletable, add delete? checkbox
- self.custom.delete = self.custom.deletable = ''
- if record and deletable:
- #add secondary css class for cascade delete warning
- css = 'delete'
- for f in self.table.fields:
- on_del = self.table[f].ondelete
- if isinstance(on_del,str) and 'cascade' in on_del.lower():
- css += ' cascade_delete'
- break
- widget = INPUT(_type='checkbox',
- _class=css,
- _id=self.FIELDKEY_DELETE_RECORD,
- _name=self.FIELDNAME_REQUEST_DELETE,
- )
- xfields.append(
- (self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_ROW_SUFFIX,
- LABEL(
- T(delete_label), separator,
- _for=self.FIELDKEY_DELETE_RECORD,
- _id=self.FIELDKEY_DELETE_RECORD + \
- SQLFORM.ID_LABEL_SUFFIX),
- widget,
- col3.get(self.FIELDKEY_DELETE_RECORD, '')))
- self.custom.delete = self.custom.deletable = widget
- # when writable, add submit button
- self.custom.submit = ''
- if not readonly:
- if 'submit' in buttons:
- widget = self.custom.submit = INPUT(_type='submit',
- _value=T(submit_button))
- elif buttons:
- widget = self.custom.submit = DIV(*buttons)
- if self.custom.submit:
- xfields.append(('submit_record' + SQLFORM.ID_ROW_SUFFIX,
- '', widget, col3.get('submit_button', '')))
- # if a record is provided and found
- # make sure it's id is stored in the form
- if record:
- if not self['hidden']:
- self['hidden'] = {}
- if not keyed:
- self['hidden']['id'] = record[table._id.name]
- (begin, end) = self._xml()
- self.custom.begin = XML("<%s %s>" % (self.tag, begin))
- self.custom.end = XML("%s</%s>" % (end, self.tag))
- table = self.createform(xfields)
- self.components = [table]
- def createform(self, xfields):
- formstyle = self.formstyle
- if isinstance(formstyle, basestring):
- if formstyle in SQLFORM.formstyles:
- formstyle = SQLFORM.formstyles[formstyle]
- else:
- raise RuntimeError('formstyle not found')
- if callable(formstyle):
- # backward compatibility, 4 argument function is the old style
- args, varargs, keywords, defaults = inspect.getargspec(formstyle)
- if defaults and len(args) - len(defaults) == 4 or len(args) == 4:
- table = TABLE()
- for id, a, b, c in xfields:
- newrows = formstyle(id, a, b, c)
- self.field_parent[id] = getattr(b, 'parent', None) \
- if isinstance(b,XmlComponent) else None
- if type(newrows).__name__ != "tuple":
- newrows = [newrows]
- for newrow in newrows:
- table.append(newrow)
- else:
- table = formstyle(self, xfields)
- for id, a, b, c in xfields:
- self.field_parent[id] = getattr(b, 'parent', None) \
- if isinstance(b,XmlComponent) else None
- else:
- raise RuntimeError('formstyle not supported')
- return table
- def accepts(
- self,
- request_vars,
- session=None,
- formname='%(tablename)s/%(record_id)s',
- keepvalues=None,
- onvalidation=None,
- dbio=True,
- hideerror=False,
- detect_record_change=False,
- **kwargs
- ):
- """
- similar FORM.accepts but also does insert, update or delete in DAL.
- but if detect_record_change == True than:
- form.record_changed = False (record is properly validated/submitted)
- form.record_changed = True (record cannot be submitted because changed)
- elseif detect_record_change == False than:
- form.record_changed = None
- """
- if keepvalues is None:
- keepvalues = True if self.record else False
- if self.readonly:
- return False
- if request_vars.__class__.__name__ == 'Request':
- request_vars = request_vars.post_vars
- keyed = hasattr(self.table, '_primarykey')
- # implement logic to detect whether record exist but has been modified
- # server side
- self.record_changed = None
- self.detect_record_change = detect_record_change
- if self.detect_record_change:
- if self.record:
- self.record_changed = False
- serialized = '|'.join(
- str(self.record[k]) for k in self.table.fields())
- self.record_hash = md5_hash(serialized)
- # logic to deal with record_id for keyed tables
- if self.record:
- if keyed:
- formname_id = '.'.join(str(self.record[k])
- for k in self.table._primarykey
- if hasattr(self.record, k))
- record_id = dict((k, request_vars.get(k, None))
- for k in self.table._primarykey)
- else:
- (formname_id, record_id) = (self.record[self.id_field_name],
- request_vars.get('id', None))
- keepvalues = True
- else:
- if keyed:
- formname_id = 'create'
- record_id = dict([(k, None) for k in self.table._primarykey])
- else:
- (formname_id, record_id) = ('create', None)
- if not keyed and isinstance(record_id, (list, tuple)):
- record_id = record_id[0]
- if formname:
- formname = formname % dict(tablename=self.table._tablename,
- record_id=formname_id)
- # ## THIS IS FOR UNIQUE RECORDS, read IS_NOT_IN_DB
- for fieldname in self.fields:
- field = self.table[fieldname]
- requires = field.requires or []
- if not isinstance(requires, (list, tuple)):
- requires = [requires]
- [item.set_self_id(self.record_id) for item in requires
- if hasattr(item, 'set_self_id') and self.record_id]
- # ## END
- fields = {}
- for key in self.vars:
- fields[key] = self.vars[key]
- ret = FORM.accepts(
- self,
- request_vars,
- session,
- formname,
- keepvalues,
- onvalidation,
- hideerror=hideerror,
- **kwargs
- )
- self.deleted = \
- request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
- self.custom.end = TAG[''](self.hidden_fields(), self.custom.end)
- auch = record_id and self.errors and self.deleted
- if self.record_changed and self.detect_record_change:
- message_onchange = \
- kwargs.setdefault("message_onchange",
- current.T("A record change was detected. " +
- "Consecutive update self-submissions " +
- "are not allowed. Try re-submitting or " +
- "refreshing the form page."))
- if message_onchange is not None:
- current.response.flash = message_onchange
- return ret
- elif (not ret) and (not auch):
- # auch is true when user tries to delete a record
- # that does not pass validation, yet it should be deleted
- for fieldname in self.fields:
- field = self.table[fieldname]
- ### this is a workaround! widgets should always have default not None!
- if not field.widget and field.type.startswith('list:') and \
- not OptionsWidget.has_options(field):
- field.widget = self.widgets.list.widget
- if field.widget and fieldname in request_vars:
- if fieldname in self.request_vars:
- value = self.request_vars[fieldname]
- elif self.record:
- value = self.record[fieldname]
- else:
- value = self.table[fieldname].default
- if field.type.startswith('list:') and isinstance(value, str):
- value = [value]
- row_id = '%s_%s%s' % (
- self.table, fieldname, SQLFORM.ID_ROW_SUFFIX)
- widget = field.widget(field, value)
- parent = self.field_parent[row_id]
- if parent:
- parent.components = [widget]
- if self.errors.get(fieldname):
- parent._traverse(False, hideerror)
- self.custom.widget[fieldname] = widget
- self.accepted = ret
- return ret
- if record_id and str(record_id) != str(self.record_id):
- raise SyntaxError('user is tampering with form\'s record_id: '
- '%s != %s' % (record_id, self.record_id))
- if record_id and dbio and not keyed:
- self.vars.id = self.record[self.id_field_name]
- if self.deleted and self.custom.deletable:
- if dbio:
- if keyed:
- qry = reduce(lambda x, y: x & y,
- [self.table[k] == record_id[k]
- for k in self.table._primarykey])
- else:
- qry = self.table._id == self.record[self.id_field_name]
- self.table._db(qry).delete()
- self.errors.clear()
- for component in self.elements('input, select, textarea'):
- component['_disabled'] = True
- self.accepted = True
- return True
- for fieldname in self.fields:
- if not fieldname in self.table.fields:
- continue
- if not self.ignore_rw and not self.table[fieldname].writable:
- ### this happens because FORM has no knowledge of writable
- ### and thinks that a missing boolean field is a None
- if self.table[fieldname].type == 'boolean' and \
- self.vars.get(fieldname, True) is None:
- del self.vars[fieldname]
- continue
- field = self.table[fieldname]
- if field.type == 'id':
- continue
- if field.type == 'boolean':
- if self.vars.get(fieldname, False):
- self.vars[fieldname] = fields[fieldname] = True
- else:
- self.vars[fieldname] = fields[fieldname] = False
- elif field.type == 'password' and self.record\
- and request_vars.get(fieldname, None) == \
- PasswordWidget.DEFAULT_PASSWORD_DISPLAY:
- continue # do not update if password was not changed
- elif field.type == 'upload':
- f = self.vars[fieldname]
- fd = '%s__delete' % fieldname
- if f == '' or f is None:
- if self.vars.get(fd, False):
- f = self.table[fieldname].default or ''
- fields[fieldname] = f
- elif self.record:
- if self.record[fieldname]:
- fields[fieldname] = self.record[fieldname]
- else:
- f = self.table[fieldname].default or ''
- fields[fieldname] = f
- else:
- f = self.table[fieldname].default or ''
- fields[fieldname] = f
- self.vars[fieldname] = fields[fieldname]
- if not f:
- continue
- else:
- f = os.path.join(
- current.request.folder,
- os.path.normpath(f))
- source_file = open(f, 'rb')
- original_filename = os.path.split(f)[1]
- elif hasattr(f, 'file'):
- (source_file, original_filename) = (f.file, f.filename)
- elif isinstance(f, (str, unicode)):
- ### do not know why this happens, it should not
- (source_file, original_filename) = \
- (cStringIO.StringIO(f), 'file.txt')
- else:
- # this should never happen, why does it happen?
- print 'f=',repr(f)
- continue
- newfilename = field.store(source_file, original_filename,
- field.uploadfolder)
- # this line was for backward compatibility but problematic
- # self.vars['%s_newfilename' % fieldname] = newfilename
- fields[fieldname] = newfilename
- if isinstance(field.uploadfield, str):
- fields[field.uploadfield] = source_file.read()
- # proposed by Hamdy (accept?) do we need fields at this point?
- self.vars[fieldname] = fields[fieldname]
- continue
- elif fieldname in self.vars:
- fields[fieldname] = self.vars[fieldname]
- elif field.default is None and field.type != 'blob':
- self.errors[fieldname] = 'no data'
- self.accepted = False
- return False
- value = fields.get(fieldname, None)
- if field.type == 'list:string':
- if not isinstance(value, (tuple, list)):
- fields[fieldname] = value and [value] or []
- elif isinstance(field.type, str) and field.type.startswith('list:'):
- if not isinstance(value, list):
- fields[fieldname] = [safe_int(
- x) for x in (value and [value] or [])]
- elif field.type == 'integer':
- if not value is None:
- fields[fieldname] = safe_int(value)
- elif field.type.startswith('reference'):
- if not value is None and isinstance(self.table, Table) and not keyed:
- fields[fieldname] = safe_int(value)
- elif field.type == 'double':
- if not value is None:
- fields[fieldname] = safe_float(value)
- for fieldname in self.vars:
- if fieldname != 'id' and fieldname in self.table.fields\
- and not fieldname in fields and not fieldname\
- in request_vars:
- fields[fieldname] = self.vars[fieldname]
- if dbio:
- if 'delete_this_record' in fields:
- # this should never happen but seems to happen to some
- del fields['delete_this_record']
- for field in self.table:
- if not field.name in fields and field.writable is False \
- and field.update is None and field.compute is None:
- if record_id and self.record:
- fields[field.name] = self.record[field.name]
- elif not self.table[field.name].default is None:
- fields[field.name] = self.table[field.name].default
- if keyed:
- if reduce(lambda x, y: x and y, record_id.values()): # if record_id
- if fields:
- qry = reduce(lambda x, y: x & y,
- [self.table[k] == self.record[k] for k in self.table._primarykey])
- self.table._db(qry).update(**fields)
- else:
- pk = self.table.insert(**fields)
- if pk:
- self.vars.update(pk)
- else:
- ret = False
- else:
- if record_id:
- self.vars.id = self.record[self.id_field_name]
- if fields:
- self.table._db(self.table._id == self.record[
- self.id_field_name]).update(**fields)
- else:
- self.vars.id = self.table.insert(**fields)
- self.accepted = ret
- return ret
- AUTOTYPES = {
- type(''): ('string', None),
- type(True): ('boolean', None),
- type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
- type(1.0): ('double', IS_FLOAT_IN_RANGE()),
- type([]): ('list:string', None),
- type(datetime.date.today()): ('date', IS_DATE()),
- type(datetime.datetime.today()): ('datetime', IS_DATETIME())
- }
- @staticmethod
- def dictform(dictionary, **kwargs):
- fields = []
- for key, value in sorted(dictionary.items()):
- t, requires = SQLFORM.AUTOTYPES.get(type(value), (None, None))
- if t:
- fields.append(Field(key, t, requires=requires,
- default=value))
- return SQLFORM.factory(*fields, **kwargs)
- @staticmethod
- def smartdictform(session, name, filename=None, query=None, **kwargs):
- import os
- if query:
- session[name] = query.db(query).select().first().as_dict()
- elif os.path.exists(filename):
- env = {'datetime': datetime}
- session[name] = eval(open(filename).read(), {}, env)
- form = SQLFORM.dictform(session[name])
- if form.process().accepted:
- session[name].update(form.vars)
- if query:
- query.db(query).update(**form.vars)
- else:
- open(filename, 'w').write(repr(session[name]))
- return form
- @staticmethod
- def factory(*fields, **attributes):
- """
- generates a SQLFORM for the given fields.
- Internally will build a non-database based data model
- to hold the fields.
- """
- # Define a table name, this way it can be logical to our CSS.
- # And if you switch from using SQLFORM to SQLFORM.factory
- # your same css definitions will still apply.
- table_name = attributes.get('table_name', 'no_table')
- # So it won't interfere with SQLDB.define_table
- if 'table_name' in attributes:
- del attributes['table_name']
- return SQLFORM(DAL(None).define_table(table_name, *fields),
- **attributes)
- @staticmethod
- def build_query(fields, keywords):
- request = current.request
- if isinstance(keywords, (tuple, list)):
- keywords = keywords[0]
- request.vars.keywords = keywords
- key = keywords.strip()
- if key and not ' ' in key and not '"' in key and not "'" in key:
- SEARCHABLE_TYPES = ('string', 'text', 'list:string')
- parts = [field.contains(
- key) for field in fields if field.type in SEARCHABLE_TYPES]
- else:
- parts = None
- if parts:
- return reduce(lambda a, b: a | b, parts)
- else:
- return smart_query(fields, key)
- @staticmethod
- def search_menu(fields,
- search_options=None,
- prefix='w2p'
- ):
- T = current.T
- panel_id='%s_query_panel' % prefix
- fields_id='%s_query_fields' % prefix
- keywords_id='%s_keywords' % prefix
- field_id='%s_field' % prefix
- value_id='%s_value' % prefix
- search_options = search_options or {
- 'string': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'],
- 'text': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'],
- 'date': ['=', '!=', '<', '>', '<=', '>='],
- 'time': ['=', '!=', '<', '>', '<=', '>='],
- 'datetime': ['=', '!=', '<', '>', '<=', '>='],
- 'integer': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'],
- 'double': ['=', '!=', '<', '>', '<=', '>='],
- 'id': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'],
- 'reference': ['=', '!='],
- 'boolean': ['=', '!=']}
- if fields[0]._db._adapter.dbengine == 'google:datastore':
- search_options['string'] = ['=', '!=', '<', '>', '<=', '>=']
- search_options['text'] = ['=', '!=', '<', '>', '<=', '>=']
- search_options['list:string'] = ['contains']
- search_options['list:integer'] = ['contains']
- search_options['list:reference'] = ['contains']
- criteria = []
- selectfields = []
- for field in fields:
- name = str(field).replace('.', '-')
- # treat ftype 'decimal' as 'double'
- # (this fixes problems but needs refactoring!
- ftype = field.type.split(' ')[0]
- if ftype.startswith('decimal'): ftype = 'double'
- elif ftype=='bigint': ftype = 'integer'
- elif ftype.startswith('big-'): ftype = ftype[4:]
- # end
- options = search_options.get(ftype, None)
- if options:
- label = isinstance(
- field.label, str) and T(field.label) or field.label
- selectfields.append(OPTION(label, _value=str(field)))
- operators = SELECT(*[OPTION(T(option), _value=option) for option in options])
- _id = "%s_%s" % (value_id,name)
- if field.type == 'boolean':
- value_input = SQLFORM.widgets.boolean.widget(field,field.default,_id=_id)
- elif field.type == 'double':
- value_input = SQLFORM.widgets.double.widget(field,field.default,_id=_id)
- elif field.type == 'time':
- value_input = SQLFORM.widgets.time.widget(field,field.default,_id=_id)
- elif field.type == 'date':
- value_input = SQLFORM.widgets.date.widget(field,field.default,_id=_id)
- elif field.type == 'datetime':
- value_input = SQLFORM.widgets.datetime.widget(field,field.default,_id=_id)
- elif (field.type.startswith('reference ') or
- field.type.startswith('list:reference ')) and \
- hasattr(field.requires,'options'):
- value_input = SELECT(
- *[OPTION(v, _value=k)
- for k,v in field.requires.options()],
- **dict(_id=_id))
- elif field.type == 'integer' or \
- field.type.startswith('reference ') or \
- field.type.startswith('list:integer') or \
- field.type.startswith('list:reference '):
- value_input = SQLFORM.widgets.integer.widget(field,field.default,_id=_id)
- else:
- value_input = INPUT(
- _type='text', _id=_id, _class=field.type)
- new_button = INPUT(
- _type="button", _value=T('New'), _class="btn",
- _onclick="%s_build_query('new','%s')" % (prefix,field))
- and_button = INPUT(
- _type="button", _value=T('And'), _class="btn",
- _onclick="%s_build_query('and','%s')" % (prefix, field))
- or_button = INPUT(
- _type="button", _value=T('Or'), _class="btn",
- _onclick="%s_build_query('or','%s')" % (prefix, field))
- close_button = INPUT(
- _type="button", _value=T('Close'), _class="btn",
- _onclick="jQuery('#%s').slideUp()" % panel_id)
- criteria.append(DIV(
- operators, value_input, new_button,
- and_button, or_button, close_button,
- _id='%s_%s' % (field_id, name),
- _class='w2p_query_row hidden',
- _style='display:inline'))
- criteria.insert(0, SELECT(
- _id=fields_id,
- _onchange="jQuery('.w2p_query_row').hide();jQuery('#%s_'+jQuery('#%s').val().replace('.','-')).show();" % (field_id,fields_id),
- _style='float:left',
- *selectfields))
- fadd = SCRIPT("""
- jQuery('#%(fields_id)s input,#%(fields_id)s select').css(
- 'width','auto');
- jQuery(function(){web2py_ajax_fields('#%(fields_id)s');});
- function %(prefix)s_build_query(aggregator,a) {
- var b=a.replace('.','-');
- var option = jQuery('#%(field_id)s_'+b+' select').val();
- var value = jQuery('#%(value_id)s_'+b).val().replace('"','\\\\"');
- var s=a+' '+option+' "'+value+'"';
- var k=jQuery('#%(keywords_id)s');
- var v=k.val();
- if(aggregator=='new') k.val(s); else k.val((v?(v+' '+ aggregator +' '):'')+s);
- }
- """ % dict(
- prefix=prefix,fields_id=fields_id,keywords_id=keywords_id,
- field_id=field_id,value_id=value_id
- )
- )
- return CAT(
- DIV(_id=panel_id, _style="display:none;", *criteria), fadd)
- @staticmethod
- def grid(query,
- fields=None,
- field_id=None,
- left=None,
- headers={},
- orderby=None,
- groupby=None,
- searchable=True,
- sortable=True,
- paginate=20,
- deletable=True,
- editable=True,
- details=True,
- selectable=None,
- create=True,
- csv=True,
- links=None,
- links_in_grid=True,
- upload='<default>',
- args=[],
- user_signature=True,
- maxtextlengths={},
- maxtextlength=20,
- onvalidation=None,
- onfailure=None,
- oncreate=None,
- onupdate=None,
- ondelete=None,
- sorter_icons=(XML('↑'), XML('↓')),
- ui = 'web2py',
- showbuttontext=True,
- _class="web2py_grid",
- formname='web2py_grid',
- search_widget='default',
- ignore_rw = False,
- formstyle = 'table3cols',
- exportclasses = None,
- formargs={},
- createargs={},
- editargs={},
- viewargs={},
- selectable_submit_button='Submit',
- buttons_placement = 'right',
- links_placement = 'right',
- noconfirm=False,
- cache_count=None,
- client_side_delete=False,
- ):
- # jQuery UI ThemeRoller classes (empty if ui is disabled)
- if ui == 'jquery-ui':
- ui = dict(widget='ui-widget',
- header='ui-widget-header',
- content='ui-widget-content',
- default='ui-state-default',
- cornerall='ui-corner-all',
- cornertop='ui-corner-top',
- cornerbottom='ui-corner-bottom',
- button='ui-button-text-icon-primary',
- buttontext='ui-button-text',
- buttonadd='ui-icon ui-icon-plusthick',
- buttonback='ui-icon ui-icon-arrowreturnthick-1-w',
- buttonexport='ui-icon ui-icon-transferthick-e-w',
- buttondelete='ui-icon ui-icon-trash',
- buttonedit='ui-icon ui-icon-pencil',
- buttontable='ui-icon ui-icon-triangle-1-e',
- buttonview='ui-icon ui-icon-zoomin',
- )
- elif ui == 'web2py':
- ui = dict(widget='',
- header='',
- content='',
- default='',
- cornerall='',
- cornertop='',
- cornerbottom='',
- button='button btn',
- buttontext='buttontext button',
- buttonadd='icon plus icon-plus',
- buttonback='icon leftarrow icon-arrow-left',
- buttonexport='icon downarrow icon-download',
- buttondelete='icon trash icon-trash',
- buttonedit='icon pen icon-pencil',
- buttontable='icon rightarrow icon-arrow-right',
- buttonview='icon magnifier icon-zoom-in',
- )
- elif not isinstance(ui, dict):
- raise RuntimeError('SQLFORM.grid ui argument must be a dictionary')
- db = query._db
- T = current.T
- request = current.request
- session = current.session
- response = current.response
- logged = session.auth and session.auth.user
- wenabled = (not user_signature or logged)
- create = wenabled and create
- editable = wenabled and editable
- deletable = wenabled and deletable
- rows = None
- def fetch_count(dbset):
- ##FIXME for google:datastore cache_count is ignored
- ## if it's not an integer
- if cache_count is None or isinstance(cache_count, tuple):
- if groupby:
- c = 'count(*)'
- nrows = db.executesql(
- 'select count(*) from (%s);' %
- dbset._select(c, left=left, cacheable=True,
- groupby=groupby, cache=cache_count)[:-1])[0][0]
- elif left:
- c = 'count(*)'
- nrows = dbset.select(c, left=left, cacheable=True, cache=cache_count).first()[c]
- elif dbset._db._adapter.dbengine=='google:datastore':
- #if we don't set a limit, this can timeout for a large table
- nrows = dbset.db._adapter.count(dbset.query, limit=1000)
- else:
- nrows = dbset.count(cache=cache_count)
- elif isinstance(cache_count, (int, long)):
- nrows = cache_count
- elif callable(cache_count):
- nrows = cache_count(dbset, request.vars)
- else:
- nrows = 0
- return nrows
- def url(**b):
- b['args'] = args + b.get('args', [])
- localvars = request.get_vars.copy()
- localvars.update(b.get('vars', {}))
- b['vars'] = localvars
- b['hash_vars'] = False
- b['user_signature'] = user_signature
- return URL(**b)
- def url2(**b):
- b['args'] = request.args + b.get('args', [])
- localvars = request.get_vars.copy()
- localvars.update(b.get('vars', {}))
- b['vars'] = localvars
- b['hash_vars'] = False
- b['user_signature'] = user_signature
- return URL(**b)
- referrer = session.get('_web2py_grid_referrer_' + formname, url())
- # if not user_signature every action is accessible
- # else forbid access unless
- # - url is based url
- # - url has valid signature (vars are not signed, only path_info)
- # = url does not contain 'create','delete','edit' (readonly)
- if user_signature:
- if not (
- '/'.join(str(a) for a in args) == '/'.join(request.args) or
- URL.verify(request,user_signature=user_signature,
- hash_vars=False) or
- (request.args(len(args))=='view' and not logged)):
- session.flash = T('not authorized')
- redirect(referrer)
- def gridbutton(buttonclass='buttonadd', buttontext=T('Add'),
- buttonurl=url(args=[]), callback=None,
- delete=None, trap=True, noconfirm=None):
- if showbuttontext:
- return A(SPAN(_class=ui.get(buttonclass)),
- SPAN(T(buttontext), _title=T(buttontext),
- _class=ui.get('buttontext')),
- _href=buttonurl,
- callback=callback,
- delete=delete,
- noconfirm=noconfirm,
- _class=trap_class(ui.get('button'), trap))
- else:
- return A(SPAN(_class=ui.get(buttonclass)),
- _href=buttonurl,
- callback=callback,
- delete=delete,
- noconfirm=noconfirm,
- _title=T(buttontext),
- _class=trap_class(ui.get('buttontext'), trap))
- dbset = db(query)
- tablenames = db._adapter.tables(dbset.query)
- if left is not None:
- if not isinstance(left, (list, tuple)):
- left = [left]
- for join in left:
- tablenames += db._adapter.tables(join)
- tables = [db[tablename] for tablename in tablenames]
- if fields:
- columns = [f for f in fields if f.tablename in tablenames]
- else:
- fields = []
- columns = []
- for table in tables:
- fields += [f for f in table]
- columns += [f for f in table]
- for k,f in table.iteritems():
- if isinstance(f,Field.Virtual) and f.readable:
- f.tablename = table._tablename
- columns.append(f)
- fields.append(f)
- if not field_id:
- field_id = tables[0]._id
- if not any(str(f)==str(field_id) for f in fields):
- fields = [f for f in fields]+[field_id]
- table = field_id.table
- tablename = table._tablename
- if upload == '<default>':
- upload = lambda filename: url(args=['download', filename])
- if request.args(-2) == 'download':
- stream = response.download(request, db)
- raise HTTP(200, stream, **response.headers)
- def buttons(edit=False, view=False, record=None):
- buttons = DIV(gridbutton('buttonback', 'Back', referrer),
- _class='form_header row_buttons %(header)s %(cornertop)s' % ui)
- if edit and (not callable(edit) or edit(record)):
- args = ['edit', table._tablename, request.args[-1]]
- buttons.append(gridbutton('buttonedit', 'Edit',
- url(args=args)))
- if view:
- args = ['view', table._tablename, request.args[-1]]
- buttons.append(gridbutton('buttonview', 'View',
- url(args=args)))
- if record and links:
- for link in links:
- if isinstance(link, dict):
- buttons.append(link['body'](record))
- elif link(record):
- buttons.append(link(record))
- return buttons
- def linsert(lst, i, x):
- """
- a = [1,2]
- linsert(a, 1, [0,3])
- a = [1, 0, 3, 2]
- """
- lst[i:i] = x
- formfooter = DIV(
- _class='form_footer row_buttons %(header)s %(cornerbottom)s' % ui)
- create_form = update_form = view_form = search_form = None
- sqlformargs = dict(formargs)
- if create and request.args(-2) == 'new':
- table = db[request.args[-1]]
- sqlformargs.update(createargs)
- create_form = SQLFORM(
- table, ignore_rw=ignore_rw, formstyle=formstyle,
- _class='web2py_form',
- **sqlformargs)
- create_form.process(formname=formname,
- next=referrer,
- onvalidation=onvalidation,
- onfailure=onfailure,
- onsuccess=oncreate)
- res = DIV(buttons(), create_form, formfooter, _class=_class)
- res.create_form = create_form
- res.update_form = update_form
- res.view_form = view_form
- res.search_form = search_form
- res.rows = None
- return res
- elif details and request.args(-3) == 'view':
- table = db[request.args[-2]]
- record = table(request.args[-1]) or redirect(referrer)
- sqlformargs.update(viewargs)
- view_form = SQLFORM(
- table, record, upload=upload, ignore_rw=ignore_rw,
- formstyle=formstyle, readonly=True, _class='web2py_form',
- **sqlformargs)
- res = DIV(buttons(edit=editable, record=record), view_form,
- formfooter, _class=_class)
- res.create_form = create_form
- res.update_form = update_form
- res.view_form = view_form
- res.search_form = search_form
- res.rows = None
- return res
- elif editable and request.args(-3) == 'edit':
- table = db[request.args[-2]]
- record = table(request.args[-1]) or redirect(URL('error'))
- sqlformargs.update(editargs)
- deletable_ = deletable(record) if callable(deletable) else deletable
- update_form = SQLFORM(
- table,
- record, upload=upload, ignore_rw=ignore_rw,
- formstyle=formstyle, deletable=deletable_,
- _class='web2py_form',
- submit_button=T('Submit'),
- delete_label=T('Check to delete'),
- **sqlformargs)
- update_form.process(
- formname=formname,
- onvalidation=onvalidation,
- onfailure=onfailure,
- onsuccess=onupdate,
- next=referrer)
- res = DIV(buttons(view=details, record=record),
- update_form, formfooter, _class=_class)
- res.create_form = create_form
- res.update_form = update_form
- res.view_form = view_form
- res.search_form = search_form
- res.rows = None
- return res
- elif deletable and request.args(-3) == 'delete':
- table = db[request.args[-2]]
- if not callable(deletable):
- if ondelete:
- ondelete(table, request.args[-1])
- db(table[table._id.name] == request.args[-1]).delete()
- else:
- record = table(request.args[-1]) or redirect(URL('error'))
- if deletable(record):
- if ondelete:
- ondelete(table, request.args[-1])
- record.delete_record()
- redirect(referrer, client_side=client_side_delete)
- exportManager = dict(
- csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'),
- csv=(ExporterCSV, 'CSV'),
- xml=(ExporterXML, 'XML'),
- html=(ExporterHTML, 'HTML'),
- json=(ExporterJSON, 'JSON'),
- tsv_with_hidden_cols=
- (ExporterTSV, 'TSV (Excel compatible, hidden cols)'),
- tsv=(ExporterTSV, 'TSV (Excel compatible)'))
- if not exportclasses is None:
- """
- remember: allow to set exportclasses=dict(csv=False) to disable the csv format
- """
- exportManager.update(exportclasses)
- export_type = request.vars._export_type
- if export_type:
- order = request.vars.order or ''
- if sortable:
- if order and not order == 'None':
- if order[:1] == '~':
- sign, rorder = '~', order[1:]
- else:
- sign, rorder = '', order
- tablename, fieldname = rorder.split('.', 1)
- orderby = db[tablename][fieldname]
- if sign == '~':
- orderby = ~orderby
- expcolumns = [str(f) for f in columns]
- if export_type.endswith('with_hidden_cols'):
- expcolumns = []
- for table in tables:
- for field in table:
- if field.readable and field.tablename in tablenames:
- expcolumns.append(field)
- if export_type in exportManager and exportManager[export_type]:
- if request.vars.keywords:
- try:
- dbset = dbset(SQLFORM.build_query(
- fields, request.vars.get('keywords', '')))
- rows = dbset.select(cacheable=True, *expcolumns)
- except Exception, e:
- response.flash = T('Internal Error')
- rows = []
- else:
- rows = dbset.select(left=left, orderby=orderby,
- cacheable=True, *expcolumns)
- value = exportManager[export_type]
- clazz = value[0] if hasattr(value, '__getitem__') else value
- oExp = clazz(rows)
- filename = '.'.join(('rows', oExp.file_ext))
- response.headers['Content-Type'] = oExp.content_type
- response.headers['Content-Disposition'] = \
- 'attachment;filename=' + filename + ';'
- raise HTTP(200, oExp.export(), **response.headers)
- elif request.vars.records and not isinstance(
- request.vars.records, list):
- request.vars.records = [request.vars.records]
- elif not request.vars.records:
- request.vars.records = []
- session['_web2py_grid_referrer_' + formname] = \
- url2(vars=request.get_vars)
- console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui)
- error = None
- if create:
- add = gridbutton(
- buttonclass='buttonadd',
- buttontext=T('Add'),
- buttonurl=url(args=['new', tablename]))
- if not searchable:
- console.append(add)
- else:
- add = ''
- if searchable:
- sfields = reduce(lambda a, b: a + b,
- [[f for f in t if f.readable] for t in tables])
- if isinstance(search_widget, dict):
- search_widget = search_widget[tablename]
- if search_widget == 'default':
- prefix = formname == 'web2py_grid' and 'w2p' or 'w2p_%s' % formname
- search_menu = SQLFORM.search_menu(sfields, prefix=prefix)
- spanel_id = '%s_query_fields' % prefix
- sfields_id = '%s_query_panel' % prefix
- skeywords_id = '%s_keywords' % prefix
- search_widget = lambda sfield, url: CAT(FORM(
- INPUT(_name='keywords', _value=request.vars.keywords,
- _id=skeywords_id,
- _onfocus="jQuery('#%s').change();jQuery('#%s').slideDown();" % (spanel_id, sfields_id)),
- INPUT(_type='submit', _value=T('Search'), _class="btn"),
- INPUT(_type='submit', _value=T('Clear'), _class="btn",
- _onclick="jQuery('#%s').val('');" % skeywords_id),
- _method="GET", _action=url), search_menu)
- form = search_widget and search_widget(sfields, url()) or ''
- console.append(add)
- console.append(form)
- keywords = request.vars.get('keywords', '')
- try:
- if callable(searchable):
- subquery = searchable(sfields, keywords)
- else:
- subquery = SQLFORM.build_query(sfields, keywords)
- except RuntimeError:
- subquery = None
- error = T('Invalid query')
- else:
- subquery = None
- if subquery:
- dbset = dbset(subquery)
- try:
- nrows = fetch_count(dbset)
- except:
- nrows = 0
- error = T('Unsupported query')
- order = request.vars.order or ''
- if sortable:
- if order and not order == 'None':
- tablename, fieldname = order.split('~')[-1].split('.', 1)
- sort_field = db[tablename][fieldname]
- exception = sort_field.type in ('date', 'datetime', 'time')
- if exception:
- orderby = (order[:1] == '~' and sort_field) or ~sort_field
- else:
- orderby = (order[:1] == '~' and ~sort_field) or sort_field
- headcols = []
- if selectable:
- headcols.append(TH(_class=ui.get('default')))
- for field in columns:
- if not field.readable:
- continue
- key = str(field)
- header = headers.get(str(field), field.label or key)
- if sortable and not isinstance(field, Field.Virtual):
- if key == order:
- key, marker = '~' + order, sorter_icons[0]
- elif key == order[1:]:
- marker = sorter_icons[1]
- else:
- marker = ''
- header = A(header, marker, _href=url(vars=dict(
- keywords=request.vars.keywords or '',
- order=key)), _class=trap_class())
- headcols.append(TH(header, _class=ui.get('default')))
- toadd = []
- if links and links_in_grid:
- for link in links:
- if isinstance(link, dict):
- toadd.append(TH(link['header'], _class=ui.get('default')))
- if links_placement in ['right', 'both']:
- headcols.extend(toadd)
- if links_placement in ['left', 'both']:
- linsert(headcols, 0, toadd)
- # Include extra column for buttons if needed.
- include_buttons_column = (details or editable or deletable or
- (links and links_in_grid and
- not all([isinstance(link, dict) for link in links])))
- if include_buttons_column:
- if buttons_placement in ['right', 'both']:
- headcols.append(TH(_class=ui.get('default','')))
- if buttons_placement in ['left', 'both']:
- headcols.insert(0, TH(_class=ui.get('default','')))
- head = TR(*headcols, **dict(_class=ui.get('header')))
- cursor = True
- #figure out what page we are one to setup the limitby
- if paginate and dbset._db._adapter.dbengine=='google:datastore':
- cursor = request.vars.cursor or True
- limitby = (0, paginate)
- try: page = int(request.vars.page or 1)-1
- except ValueError: page = 0
- elif paginate and paginate<nrows:
- try: page = int(request.vars.page or 1)-1
- except ValueError: page = 0
- limitby = (paginate*page,paginate*(page+1))
- else:
- limitby = None
- try:
- table_fields = filter(lambda f: f.tablename in tablenames, fields)
- if dbset._db._adapter.dbengine=='google:datastore':
- rows = dbset.select(left=left,orderby=orderby,
- groupby=groupby,limitby=limitby,
- reusecursor=cursor,
- cacheable=True,*table_fields)
- next_cursor = dbset._db.get('_lastcursor', None)
- else:
- # print('table_fields: %s' %([f_.name for f_ in table_fields],))
- rows = dbset.select(left=left,orderby=orderby,
- groupby=groupby,limitby=limitby,
- cacheable=True,*table_fields)
- except SyntaxError:
- rows = None
- next_cursor = None
- error = T("Query Not Supported")
- except Exception, e:
- rows = None
- next_cursor = None
- error = T("Query Not Supported: %s")%e
- message = error
- if not message and nrows:
- if dbset._db._adapter.dbengine=='google:datastore' and nrows>=1000:
- message = T('at least %(nrows)s records found') % dict(nrows=nrows)
- else:
- message = T('%(nrows)s records found') % dict(nrows=nrows)
- console.append(DIV(message or T('None'),_class='web2py_counter'))
- paginator = UL()
- if paginate and dbset._db._adapter.dbengine=='google:datastore':
- #this means we may have a large table with an unknown number of rows.
- try:
- page = int(request.vars.page or 1)-1
- except ValueError:
- page = 0
- paginator.append(LI('page %s'%(page+1)))
- if next_cursor:
- d = dict(page=page+2, cursor=next_cursor)
- if order: d['order']=order
- if request.vars.keywords: d['keywords']=request.vars.keywords
- paginator.append(LI(
- A('next',_href=url(vars=d),_class=trap_class())))
- elif paginate and paginate<nrows:
- npages, reminder = divmod(nrows, paginate)
- if reminder:
- npages += 1
- try:
- page = int(request.vars.page or 1) - 1
- except ValueError:
- page = 0
- def self_link(name, p):
- d = dict(page=p + 1)
- if order:
- d['order'] = order
- if request.vars.keywords:
- d['keywords'] = request.vars.keywords
- return A(name, _href=url(vars=d), _class=trap_class())
- NPAGES = 5 # window is 2*NPAGES
- if page > NPAGES + 1:
- paginator.append(LI(self_link('<<', 0)))
- if page > NPAGES:
- paginator.append(LI(self_link('<', page - 1)))
- pages = range(max(0, page - NPAGES), min(page + NPAGES, npages))
- for p in pages:
- if p == page:
- paginator.append(LI(A(p + 1, _onclick='return false'),
- _class=trap_class('current')))
- else:
- paginator.append(LI(self_link(p + 1, p)))
- if page < npages - NPAGES:
- paginator.append(LI(self_link('>', page + 1)))
- if page < npages - NPAGES - 1:
- paginator.append(LI(self_link('>>', npages - 1)))
- else:
- limitby = None
- if rows:
- htmltable = TABLE(THEAD(head))
- tbody = TBODY()
- numrec = 0
- for row in rows:
- trcols = []
- id = row[field_id]
- if selectable:
- trcols.append(
- INPUT(_type="checkbox", _name="records", _value=id,
- value=request.vars.records))
- for field in columns:
- if not field.readable:
- continue
- if field.type == 'blob':
- continue
- value = row[str(field)]
- maxlength = maxtextlengths.get(str(field), maxtextlength)
- if field.represent:
- try:
- value = field.represent(value, row)
- except KeyError:
- try:
- value = field.represent(
- value, row[field.tablename])
- except KeyError:
- pass
- elif field.type == 'boolean':
- value = INPUT(_type="checkbox", _checked=value,
- _disabled=True)
- elif field.type == 'upload':
- if value:
- if callable(upload):
- value = A(
- T('file'), _href=upload(value))
- elif upload:
- value = A(T('file'),
- _href='%s/%s' % (upload, value))
- else:
- value = ''
- if isinstance(value, str):
- value = truncate_string(value, maxlength)
- elif not isinstance(value, DIV):
- value = field.formatter(value)
- trcols.append(TD(value))
- row_buttons = TD(_class='row_buttons',_nowrap=True)
- if links and links_in_grid:
- toadd = []
- for link in links:
- if isinstance(link, dict):
- toadd.append(TD(link['body'](row)))
- else:
- if link(row):
- row_buttons.append(link(row))
- if links_placement in ['right', 'both']:
- trcols.extend(toadd)
- if links_placement in ['left', 'both']:
- linsert(trcols, 0, toadd)
- if include_buttons_column:
- if details and (not callable(details) or details(row)):
- row_buttons.append(gridbutton(
- 'buttonview', 'View',
- url(args=['view', tablename, id])))
- if editable and (not callable(editable) or editable(row)):
- row_buttons.append(gridbutton(
- 'buttonedit', 'Edit',
- url(args=['edit', tablename, id])))
- if deletable and (not callable(deletable) or deletable(row)):
- row_buttons.append(gridbutton(
- 'buttondelete', 'Delete',
- url(args=['delete', tablename, id]),
- callback=url(args=['delete', tablename, id]),
- noconfirm=noconfirm,
- delete='tr'))
- if buttons_placement in ['right', 'both']:
- trcols.append(row_buttons)
- if buttons_placement in ['left', 'both']:
- trcols.insert(0, row_buttons)
- if numrec % 2 == 0:
- classtr = 'even'
- else:
- classtr = 'odd'
- numrec += 1
- if id:
- rid = id
- if callable(rid): # can this ever be callable?
- rid = rid(row)
- tr = TR(*trcols, **dict(
- _id=rid,
- _class='%s %s' % (classtr, 'with_id')))
- else:
- tr = TR(*trcols, **dict(_class=classtr))
- tbody.append(tr)
- htmltable.append(tbody)
- htmltable = DIV(
- htmltable, _class='web2py_htmltable',
- _style='width:100%;overflow-x:auto;-ms-overflow-x:scroll')
- if selectable:
- if not callable(selectable):
- #now expect that selectable and related parameters are iterator (list, tuple, etc)
- inputs = []
- for i, submit_info in enumerate(selectable):
- submit_text = submit_info[0]
- submit_class = submit_info[2] if len(submit_info) > 2 else ''
- input_ctrl = INPUT(_type="submit", _name='submit_%d' % i, _value=T(submit_text))
- input_ctrl.add_class(submit_class)
- inputs.append(input_ctrl)
- else:
- inputs = [INPUT(_type="submit", _value=T(selectable_submit_button))]
- if formstyle == 'bootstrap':
- # add space between buttons
- #inputs = sum([[inp, ' '] for inp in inputs], [])[:-1]
- htmltable = FORM(htmltable, DIV(_class='form-actions', *inputs))
- else:
- htmltable = FORM(htmltable, *inputs)
- if htmltable.process(formname=formname).accepted:
- htmltable.vars.records = htmltable.vars.records or []
- htmltable.vars.records = htmltable.vars.records if type(htmltable.vars.records) == list else [htmltable.vars.records]
- records = [int(r) for r in htmltable.vars.records]
- if not callable(selectable):
- for i, submit_info in enumerate(selectable):
- submit_callback = submit_info[1]
- if htmltable.vars.get('submit_%d' % i, False):
- submit_callback(records)
- break
- else:
- selectable(records)
- redirect(referrer)
- else:
- htmltable = DIV(T('No records found'))
- if csv and nrows:
- export_links = []
- for k, v in sorted(exportManager.items()):
- if not v:
- continue
- label = v[1] if hasattr(v, "__getitem__") else k
- link = url2(vars=dict(
- order=request.vars.order or '',
- _export_type=k,
- keywords=request.vars.keywords or ''))
- export_links.append(A(T(label), _href=link))
- export_menu = \
- DIV(T('Export:'), _class="w2p_export_menu", *export_links)
- else:
- export_menu = None
- res = DIV(console, DIV(htmltable, _class="web2py_table"),
- _class='%s %s' % (_class, ui.get('widget')))
- if paginator.components:
- res.append(
- DIV(paginator,
- _class="web2py_paginator %(header)s %(cornerbottom)s" % ui))
- if export_menu:
- res.append(export_menu)
- res.create_form = create_form
- res.update_form = update_form
- res.view_form = view_form
- res.search_form = search_form
- res.rows = rows
- return res
- @staticmethod
- def smartgrid(table, constraints=None, linked_tables=None,
- links=None, links_in_grid=True,
- args=None, user_signature=True,
- divider='>', breadcrumbs_class='',
- **kwargs):
- """
- @auth.requires_login()
- def index():
- db.define_table('person',Field('name'),format='%(name)s')
- db.define_table('dog',
- Field('name'),Field('owner',db.person),format='%(name)s')
- db.define_table('comment',Field('body'),Field('dog',db.dog))
- if db(db.person).isempty():
- from gluon.contrib.populate import populate
- populate(db.person,300)
- populate(db.dog,300)
- populate(db.comment,1000)
- db.commit()
- form=SQLFORM.smartgrid(db[request.args(0) or 'person']) #***
- return dict(form=form)
- *** builds a complete interface to navigate all tables links
- to the request.args(0)
- table: pagination, search, view, edit, delete,
- children, parent, etc.
- constraints is a dict {'table':query} that limits which
- records can be accessible
- links is a dict like
- {'tablename':[lambda row: A(....), ...]}
- that will add buttons when table tablename is displayed
- linked_tables is a optional list of tablenames of tables
- to be linked
- """
- request, T = current.request, current.T
- if args is None:
- args = []
- def url(**b):
- b['args'] = request.args[:nargs] + b.get('args', [])
- b['hash_vars'] = False
- b['user_signature'] = user_signature
- return URL(**b)
- db = table._db
- breadcrumbs = []
- if request.args(len(args)) != table._tablename:
- request.args[:] = args + [table._tablename]
- if links is None:
- links = {}
- if constraints is None:
- constraints = {}
- field = None
- name = None
- def format(table,row):
- if not row:
- return T('Unknown')
- elif isinstance(table._format,str):
- return table._format % row
- elif callable(table._format):
- return table._format(row)
- else:
- return '#'+str(row.id)
- try:
- nargs = len(args) + 1
- previous_tablename, previous_fieldname, previous_id = \
- table._tablename, None, None
- while len(request.args) > nargs:
- key = request.args(nargs)
- if '.' in key:
- id = request.args(nargs + 1)
- tablename, fieldname = key.split('.', 1)
- table = db[tablename]
- field = table[fieldname]
- field.default = id
- referee = field.type[10:]
- if referee != previous_tablename:
- raise HTTP(400)
- cond = constraints.get(referee, None)
- if cond:
- record = db(
- db[referee]._id == id)(cond).select().first()
- else:
- record = db[referee](id)
- if previous_id:
- if record[previous_fieldname] != int(previous_id):
- raise HTTP(400)
- previous_tablename, previous_fieldname, previous_id = \
- tablename, fieldname, id
- name = format(db[referee],record)
- breadcrumbs.append(
- LI(A(T(db[referee]._plural),
- _class=trap_class(),
- _href=url()),
- SPAN(divider, _class='divider'),
- _class='w2p_grid_breadcrumb_elem'))
- if kwargs.get('details', True):
- breadcrumbs.append(
- LI(A(name, _class=trap_class(),
- _href=url(args=['view', referee, id])),
- SPAN(divider, _class='divider'),
- _class='w2p_grid_breadcrumb_elem'))
- nargs += 2
- else:
- break
- if nargs > len(args) + 1:
- query = (field == id)
- # cjk
- # if isinstance(linked_tables, dict):
- # linked_tables = linked_tables.get(table._tablename, [])
- if linked_tables is None or referee in linked_tables:
- 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]))
- except (KeyError, ValueError, TypeError):
- redirect(URL(args=table._tablename))
- if nargs == len(args) + 1:
- query = table._db._adapter.id_query(table)
- # filter out data info for displayed table
- if table._tablename in constraints:
- query = query & constraints[table._tablename]
- if isinstance(links, dict):
- links = links.get(table._tablename, [])
- for key in 'columns,orderby,searchable,sortable,paginate,deletable,editable,details,selectable,create,fields'.split(','):
- if isinstance(kwargs.get(key, None), dict):
- if table._tablename in kwargs[key]:
- kwargs[key] = kwargs[key][table._tablename]
- else:
- del kwargs[key]
- check = {}
- id_field_name = table._id.name
- for rfield in table._referenced_by:
- check[rfield.tablename] = \
- check.get(rfield.tablename, []) + [rfield.name]
- if linked_tables is None:
- linked_tables = db.tables()
- if isinstance(linked_tables, dict):
- linked_tables = linked_tables.get(table._tablename,[])
- if linked_tables:
- for item in linked_tables:
- tb = None
- if isinstance(item,Table) and item._tablename in check:
- tablename = item._tablename
- linked_fieldnames = check[tablename]
- td = item
- elif isinstance(item,str) and item in check:
- tablename = item
- linked_fieldnames = check[item]
- tb = db[item]
- elif isinstance(item,Field) and item.name in check.get(item._tablename,[]):
- tablename = item._tablename
- linked_fieldnames = [item.name]
- tb = item.table
- else:
- linked_fieldnames = []
- if tb:
- multiple_links = len(linked_fieldnames) > 1
- for fieldname in linked_fieldnames:
- t = T(tb._plural) if not multiple_links else \
- T(tb._plural + '(' + fieldname + ')')
- args0 = tablename + '.' + fieldname
- links.append(
- lambda row, t=t, nargs=nargs, args0=args0:
- A(SPAN(t), _class=trap_class(), _href=url(
- args=[args0, row[id_field_name]])))
- grid = SQLFORM.grid(query, args=request.args[:nargs], links=links,
- links_in_grid=links_in_grid,
- user_signature=user_signature, **kwargs)
- if isinstance(grid, DIV):
- header = table._plural
- next = grid.create_form or grid.update_form or grid.view_form
- breadcrumbs.append(LI(
- A(T(header), _class=trap_class(),_href=url()),
- SPAN(divider, _class='divider') if next else '',
- _class='active w2p_grid_breadcrumb_elem'))
- if grid.create_form:
- header = T('New %(entity)s') % dict(entity=table._singular)
- elif grid.update_form:
- header = T('Edit %(entity)s') % dict(
- entity=format(grid.update_form.table,
- grid.update_form.record))
- elif grid.view_form:
- header = T('View %(entity)s') % dict(
- entity=format(grid.view_form.table,
- grid.view_form.record))
- if next:
- breadcrumbs.append(LI(
- A(T(header), _class=trap_class(),_href=url()),
- _class='active w2p_grid_breadcrumb_elem'))
- grid.insert(
- 0, DIV(UL(*breadcrumbs, **{'_class': breadcrumbs_class}),
- _class='web2py_breadcrumbs'))
- return grid
- class SQLTABLE(TABLE):
- """
- given a Rows object, as returned by a db().select(), generates
- an html table with the rows.
- optional arguments:
- :param linkto: URL (or lambda to generate a URL) to edit individual records
- :param upload: URL to download uploaded files
- :param orderby: Add an orderby link to column headers.
- :param headers: dictionary of headers to headers redefinions
- headers can also be a string to gerenare the headers from data
- for now only headers="fieldname:capitalize",
- headers="labels" and headers=None are supported
- :param truncate: length at which to truncate text in table cells.
- Defaults to 16 characters.
- :param columns: a list or dict contaning the names of the columns to be shown
- Defaults to all
- Optional names attributes for passed to the <table> tag
- The keys of headers and columns must be of the form "tablename.fieldname"
- Simple linkto example::
- rows = db.select(db.sometable.ALL)
- table = SQLTABLE(rows, linkto='someurl')
- This will link rows[id] to .../sometable/value_of_id
- More advanced linkto example::
- def mylink(field, type, ref):
- return URL(args=[field])
- rows = db.select(db.sometable.ALL)
- table = SQLTABLE(rows, linkto=mylink)
- This will link rows[id] to
- current_app/current_controlle/current_function/value_of_id
- New Implements: 24 June 2011:
- -----------------------------
- :param selectid: The id you want to select
- :param renderstyle: Boolean render the style with the table
- :param extracolumns = [{'label':A('Extra',_href='#'),
- 'class': '', #class name of the header
- 'width':'', #width in pixels or %
- 'content':lambda row, rc: A('Edit',_href='edit/%s'%row.id),
- 'selected': False #agregate class selected to this column
- }]
- :param headers = {'table.id':{'label':'Id',
- 'class':'', #class name of the header
- 'width':'', #width in pixels or %
- 'truncate': 16, #truncate the content to...
- 'selected': False #agregate class selected to this column
- },
- 'table.myfield':{'label':'My field',
- 'class':'', #class name of the header
- 'width':'', #width in pixels or %
- 'truncate': 16, #truncate the content to...
- 'selected': False #agregate class selected to this column
- },
- }
- table = SQLTABLE(rows, headers=headers, extracolumns=extracolumns)
- `<
- """
- def __init__(
- self,
- sqlrows,
- linkto=None,
- upload=None,
- orderby=None,
- headers={},
- truncate=16,
- columns=None,
- th_link='',
- extracolumns=None,
- selectid=None,
- renderstyle=False,
- cid=None,
- **attributes
- ):
- TABLE.__init__(self, **attributes)
- self.components = []
- self.attributes = attributes
- self.sqlrows = sqlrows
- (components, row) = (self.components, [])
- if not sqlrows:
- return
- if not columns:
- columns = sqlrows.colnames
- if headers == 'fieldname:capitalize':
- headers = {}
- for c in columns:
- headers[c] = c.split('.')[-1].replace('_', ' ').title()
- elif headers == 'labels':
- headers = {}
- for c in columns:
- (t, f) = c.split('.')
- field = sqlrows.db[t][f]
- headers[c] = field.label
- if headers is None:
- headers = {}
- else:
- for c in columns: # new implement dict
- if isinstance(headers.get(c, c), dict):
- coldict = headers.get(c, c)
- attrcol = dict()
- if coldict['width'] != "":
- attrcol.update(_width=coldict['width'])
- if coldict['class'] != "":
- attrcol.update(_class=coldict['class'])
- row.append(TH(coldict['label'], **attrcol))
- elif orderby:
- row.append(TH(A(headers.get(c, c),
- _href=th_link + '?orderby=' + c, cid=cid)))
- else:
- row.append(TH(headers.get(c, c)))
- if extracolumns: # new implement dict
- for c in extracolumns:
- attrcol = dict()
- if c['width'] != "":
- attrcol.update(_width=c['width'])
- if c['class'] != "":
- attrcol.update(_class=c['class'])
- row.append(TH(c['label'], **attrcol))
- components.append(THEAD(TR(*row)))
- tbody = []
- for (rc, record) in enumerate(sqlrows):
- row = []
- if rc % 2 == 0:
- _class = 'even'
- else:
- _class = 'odd'
- if not selectid is None: # new implement
- if record.get('id') == selectid:
- _class += ' rowselected'
- for colname in columns:
- if not table_field.match(colname):
- if "_extra" in record and colname in record._extra:
- r = record._extra[colname]
- row.append(TD(r))
- continue
- else:
- raise KeyError(
- "Column %s not found (SQLTABLE)" % colname)
- (tablename, fieldname) = colname.split('.')
- try:
- field = sqlrows.db[tablename][fieldname]
- except (KeyError, AttributeError):
- field = None
- if tablename in record \
- and isinstance(record, Row) \
- and isinstance(record[tablename], Row):
- r = record[tablename][fieldname]
- elif fieldname in record:
- r = record[fieldname]
- else:
- raise SyntaxError('something wrong in Rows object')
- r_old = r
- if not field or isinstance(field, (Field.Virtual, Field.Lazy)):
- pass
- elif linkto and field.type == 'id':
- try:
- href = linkto(r, 'table', tablename)
- except TypeError:
- href = '%s/%s/%s' % (linkto, tablename, r_old)
- r = A(r, _href=href)
- elif isinstance(field.type, str) and field.type.startswith('reference'):
- if linkto:
- ref = field.type[10:]
- try:
- href = linkto(r, 'reference', ref)
- except TypeError:
- href = '%s/%s/%s' % (linkto, ref, r_old)
- if ref.find('.') >= 0:
- tref, fref = ref.split('.')
- if hasattr(sqlrows.db[tref], '_primarykey'):
- href = '%s/%s?%s' % (linkto, tref, urllib.urlencode({fref: r}))
- r = A(represent(field, r, record), _href=str(href))
- elif field.represent:
- r = represent(field, r, record)
- elif linkto and hasattr(field._table, '_primarykey')\
- and fieldname in field._table._primarykey:
- # have to test this with multi-key tables
- key = urllib.urlencode(dict([
- ((tablename in record
- and isinstance(record, Row)
- and isinstance(record[tablename], Row)) and
- (k, record[tablename][k])) or (k, record[k])
- for k in field._table._primarykey]))
- r = A(r, _href='%s/%s?%s' % (linkto, tablename, key))
- elif isinstance(field.type, str) and field.type.startswith('list:'):
- r = represent(field, r or [], record)
- elif field.represent:
- r = represent(field, r, record)
- elif field.type == 'blob' and r:
- r = 'DATA'
- elif field.type == 'upload':
- if upload and r:
- r = A(current.T('file'), _href='%s/%s' % (upload, r))
- elif r:
- r = current.T('file')
- else:
- r = ''
- elif field.type in ['string', 'text']:
- r = str(field.formatter(r))
- if headers != {}: # new implement dict
- if isinstance(headers[colname], dict):
- if isinstance(headers[colname]['truncate'], int):
- r = truncate_string(
- r, headers[colname]['truncate'])
- elif not truncate is None:
- r = truncate_string(r, truncate)
- attrcol = dict() # new implement dict
- if headers != {}:
- if isinstance(headers[colname], dict):
- colclass = headers[colname]['class']
- if headers[colname]['selected']:
- colclass = str(headers[colname]
- ['class'] + " colselected").strip()
- if colclass != "":
- attrcol.update(_class=colclass)
- row.append(TD(r, **attrcol))
- if extracolumns: # new implement dict
- for c in extracolumns:
- attrcol = dict()
- colclass = c['class']
- if c['selected']:
- colclass = str(c['class'] + " colselected").strip()
- if colclass != "":
- attrcol.update(_class=colclass)
- contentfunc = c['content']
- row.append(TD(contentfunc(record, rc), **attrcol))
- tbody.append(TR(_class=_class, *row))
- if renderstyle:
- components.append(STYLE(self.style()))
- components.append(TBODY(*tbody))
- def style(self):
- css = '''
- table tbody tr.odd {
- background-color: #DFD;
- }
- table tbody tr.even {
- background-color: #EFE;
- }
- table tbody tr.rowselected {
- background-color: #FDD;
- }
- table tbody tr td.colselected {
- background-color: #FDD;
- }
- table tbody tr:hover {
- background: #DDF;
- }
- '''
- return css
- form_factory = SQLFORM.factory # for backward compatibility, deprecated
- class ExportClass(object):
- label = None
- file_ext = None
- content_type = None
- def __init__(self, rows):
- self.rows = rows
- def represented(self):
- def none_exception(value):
- """
- returns a cleaned up value that can be used for csv export:
- - unicode text is encoded as such
- - None values are replaced with the given representation (default <NULL>)
- """
- if value is None:
- return '<NULL>'
- elif isinstance(value, unicode):
- return value.encode('utf8')
- elif isinstance(value, Reference):
- return int(value)
- elif hasattr(value, 'isoformat'):
- return value.isoformat()[:19].replace('T', ' ')
- elif isinstance(value, (list, tuple)): # for type='list:..'
- return bar_encode(value)
- return value
- represented = []
- for record in self.rows:
- row = []
- for col in self.rows.colnames:
- if not REGEX_TABLE_DOT_FIELD.match(col):
- row.append(record._extra[col])
- else:
- (t, f) = col.split('.')
- field = self.rows.db[t][f]
- if isinstance(record.get(t, None), (Row, dict)):
- value = record[t][f]
- else:
- value = record[f]
- if field.type == 'blob' and not value is None:
- value = ''
- elif field.represent:
- value = field.represent(value, record)
- row.append(none_exception(value))
- represented.append(row)
- return represented
- def export(self):
- raise NotImplementedError
- class ExporterTSV(ExportClass):
- label = 'TSV'
- file_ext = "csv"
- content_type = "text/tab-separated-values"
- def __init__(self, rows):
- ExportClass.__init__(self, rows)
- def export(self):
- out = cStringIO.StringIO()
- final = cStringIO.StringIO()
- import csv
- writer = csv.writer(out, delimiter='\t')
- if self.rows:
- import codecs
- final.write(codecs.BOM_UTF16)
- writer.writerow(
- [unicode(col).encode("utf8") for col in self.rows.colnames])
- data = out.getvalue().decode("utf8")
- data = data.encode("utf-16")
- data = data[2:]
- final.write(data)
- out.truncate(0)
- records = self.represented()
- for row in records:
- writer.writerow(
- [str(col).decode('utf8').encode("utf-8") for col in row])
- data = out.getvalue().decode("utf8")
- data = data.encode("utf-16")
- data = data[2:]
- final.write(data)
- out.truncate(0)
- return str(final.getvalue())
- class ExporterCSV(ExportClass):
- label = 'CSV'
- file_ext = "csv"
- content_type = "text/csv"
- def __init__(self, rows):
- ExportClass.__init__(self, rows)
- def export(self):
- if self.rows:
- return self.rows.as_csv()
- else:
- return ''
- class ExporterHTML(ExportClass):
- label = 'HTML'
- file_ext = "html"
- content_type = "text/html"
- def __init__(self, rows):
- ExportClass.__init__(self, rows)
- def export(self):
- if self.rows:
- return self.rows.xml()
- else:
- return '<html>\n<body>\n<table>\n</table>\n</body>\n</html>'
- class ExporterXML(ExportClass):
- label = 'XML'
- file_ext = "xml"
- content_type = "text/xml"
- def __init__(self, rows):
- ExportClass.__init__(self, rows)
- def export(self):
- if self.rows:
- return self.rows.as_xml()
- else:
- return '<rows></rows>'
- class ExporterJSON(ExportClass):
- label = 'JSON'
- file_ext = "json"
- content_type = "application/json"
- def __init__(self, rows):
- ExportClass.__init__(self, rows)
- def export(self):
- if self.rows:
- return self.rows.as_json()
- else:
- return 'null'