PageRenderTime 57ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/gluon/sqlhtml.py

https://github.com/gokceneraslan/web2py
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

Large files files are truncated, but you can click here to view the full file

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. This file is part of the web2py Web Framework
  5. Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
  6. License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
  7. Holds:
  8. - SQLFORM: provide a form for a table (with/without record)
  9. - SQLTABLE: provides a table for a set of records
  10. - form_factory: provides a SQLFORM for an non-db backed table
  11. """
  12. try:
  13. from urlparse import parse_qs as psq
  14. except ImportError:
  15. from cgi import parse_qs as psq
  16. import os
  17. import copy
  18. from http import HTTP
  19. from html import XmlComponent
  20. from html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT
  21. from html import FORM, INPUT, LABEL, OPTION, SELECT
  22. from html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE
  23. from html import URL, truncate_string, FIELDSET
  24. from dal import DAL, Field, Table, Row, CALLABLETYPES, smart_query, \
  25. bar_encode, Reference, REGEX_TABLE_DOT_FIELD
  26. from storage import Storage
  27. from utils import md5_hash
  28. from validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \
  29. IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, IS_STRONG
  30. import serializers
  31. import datetime
  32. import urllib
  33. import re
  34. import cStringIO
  35. from globals import current
  36. from http import redirect
  37. import inspect
  38. try:
  39. import settings
  40. is_gae = settings.global_settings.web2py_runtime_gae
  41. except ImportError:
  42. is_gae = False # this is an assumption (if settings missing)
  43. table_field = re.compile('[\w_]+\.[\w_]+')
  44. widget_class = re.compile('^\w*')
  45. def trap_class(_class=None, trap=True):
  46. return (trap and 'w2p_trap' or '') + (_class and ' ' + _class or '')
  47. def represent(field, value, record):
  48. f = field.represent
  49. if not callable(f):
  50. return str(value)
  51. n = f.func_code.co_argcount - len(f.func_defaults or [])
  52. if getattr(f, 'im_self', None):
  53. n -= 1
  54. if n == 1:
  55. return f(value)
  56. elif n == 2:
  57. return f(value, record)
  58. else:
  59. raise RuntimeError("field representation must take 1 or 2 args")
  60. def safe_int(x):
  61. try:
  62. return int(x)
  63. except ValueError:
  64. return 0
  65. def safe_float(x):
  66. try:
  67. return float(x)
  68. except ValueError:
  69. return 0
  70. def show_if(cond):
  71. if not cond:
  72. return None
  73. base = "%s_%s" % (cond.first.tablename, cond.first.name)
  74. if ((cond.op.__name__ == 'EQ' and cond.second == True) or
  75. (cond.op.__name__ == 'NE' and cond.second == False)):
  76. return base,":checked"
  77. if ((cond.op.__name__ == 'EQ' and cond.second == False) or
  78. (cond.op.__name__ == 'NE' and cond.second == True)):
  79. return base,":not(:checked)"
  80. if cond.op.__name__ == 'EQ':
  81. return base,"[value='%s']" % cond.second
  82. if cond.op.__name__ == 'NE':
  83. return base,"[value!='%s']" % cond.second
  84. if cond.op.__name__ == 'CONTAINS':
  85. return base,"[value~='%s']" % cond.second
  86. if cond.op.__name__ == 'BELONGS' and isinstance(cond.second,(list,tuple)):
  87. return base,','.join("[value='%s']" % (v) for v in cond.second)
  88. raise RuntimeError("Not Implemented Error")
  89. class FormWidget(object):
  90. """
  91. helper for SQLFORM to generate form input fields
  92. (widget), related to the fieldtype
  93. """
  94. _class = 'generic-widget'
  95. @classmethod
  96. def _attributes(cls, field,
  97. widget_attributes, **attributes):
  98. """
  99. helper to build a common set of attributes
  100. :param field: the field involved,
  101. some attributes are derived from this
  102. :param widget_attributes: widget related attributes
  103. :param attributes: any other supplied attributes
  104. """
  105. attr = dict(
  106. _id='%s_%s' % (field.tablename, field.name),
  107. _class=cls._class or
  108. widget_class.match(str(field.type)).group(),
  109. _name=field.name,
  110. requires=field.requires,
  111. )
  112. if getattr(field,'show_if',None):
  113. trigger, cond = show_if(field.show_if)
  114. attr['_data-show-trigger'] = trigger
  115. attr['_data-show-if'] = cond
  116. attr.update(widget_attributes)
  117. attr.update(attributes)
  118. return attr
  119. @classmethod
  120. def widget(cls, field, value, **attributes):
  121. """
  122. generates the widget for the field.
  123. When serialized, will provide an INPUT tag:
  124. - id = tablename_fieldname
  125. - class = field.type
  126. - name = fieldname
  127. :param field: the field needing the widget
  128. :param value: value
  129. :param attributes: any other attributes to be applied
  130. """
  131. raise NotImplementedError
  132. class StringWidget(FormWidget):
  133. _class = 'string'
  134. @classmethod
  135. def widget(cls, field, value, **attributes):
  136. """
  137. generates an INPUT text tag.
  138. see also: :meth:`FormWidget.widget`
  139. """
  140. default = dict(
  141. _type='text',
  142. value=(not value is None and str(value)) or '',
  143. )
  144. attr = cls._attributes(field, default, **attributes)
  145. return INPUT(**attr)
  146. class IntegerWidget(StringWidget):
  147. _class = 'integer'
  148. class DoubleWidget(StringWidget):
  149. _class = 'double'
  150. class DecimalWidget(StringWidget):
  151. _class = 'decimal'
  152. class TimeWidget(StringWidget):
  153. _class = 'time'
  154. class DateWidget(StringWidget):
  155. _class = 'date'
  156. class DatetimeWidget(StringWidget):
  157. _class = 'datetime'
  158. class TextWidget(FormWidget):
  159. _class = 'text'
  160. @classmethod
  161. def widget(cls, field, value, **attributes):
  162. """
  163. generates a TEXTAREA tag.
  164. see also: :meth:`FormWidget.widget`
  165. """
  166. default = dict(value=value)
  167. attr = cls._attributes(field, default, **attributes)
  168. return TEXTAREA(**attr)
  169. class JSONWidget(FormWidget):
  170. _class = 'json'
  171. @classmethod
  172. def widget(cls, field, value, **attributes):
  173. """
  174. generates a TEXTAREA for JSON notation.
  175. see also: :meth:`FormWidget.widget`
  176. """
  177. if not isinstance(value, basestring):
  178. if value is not None:
  179. value = serializers.json(value)
  180. default = dict(value=value)
  181. attr = cls._attributes(field, default, **attributes)
  182. return TEXTAREA(**attr)
  183. class BooleanWidget(FormWidget):
  184. _class = 'boolean'
  185. @classmethod
  186. def widget(cls, field, value, **attributes):
  187. """
  188. generates an INPUT checkbox tag.
  189. see also: :meth:`FormWidget.widget`
  190. """
  191. default = dict(_type='checkbox', value=value)
  192. attr = cls._attributes(field, default,
  193. **attributes)
  194. return INPUT(**attr)
  195. class OptionsWidget(FormWidget):
  196. @staticmethod
  197. def has_options(field):
  198. """
  199. checks if the field has selectable options
  200. :param field: the field needing checking
  201. :returns: True if the field has options
  202. """
  203. return hasattr(field.requires, 'options')
  204. @classmethod
  205. def widget(cls, field, value, **attributes):
  206. """
  207. generates a SELECT tag, including OPTIONs (only 1 option allowed)
  208. see also: :meth:`FormWidget.widget`
  209. """
  210. default = dict(value=value)
  211. attr = cls._attributes(field, default,
  212. **attributes)
  213. requires = field.requires
  214. if not isinstance(requires, (list, tuple)):
  215. requires = [requires]
  216. if requires:
  217. if hasattr(requires[0], 'options'):
  218. options = requires[0].options()
  219. else:
  220. raise SyntaxError(
  221. 'widget cannot determine options of %s' % field)
  222. opts = [OPTION(v, _value=k) for (k, v) in options]
  223. return SELECT(*opts, **attr)
  224. class ListWidget(StringWidget):
  225. @classmethod
  226. def widget(cls, field, value, **attributes):
  227. _id = '%s_%s' % (field.tablename, field.name)
  228. _name = field.name
  229. if field.type == 'list:integer':
  230. _class = 'integer'
  231. else:
  232. _class = 'string'
  233. requires = field.requires if isinstance(
  234. field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None
  235. nvalue = value or ['']
  236. items = [LI(INPUT(_id=_id, _class=_class, _name=_name,
  237. value=v, hideerror=k < len(nvalue) - 1,
  238. requires=requires),
  239. **attributes) for (k, v) in enumerate(nvalue)]
  240. attributes['_id'] = _id + '_grow_input'
  241. attributes['_style'] = 'list-style:none'
  242. attributes['_class'] = 'w2p_list'
  243. return TAG[''](UL(*items, **attributes))
  244. class MultipleOptionsWidget(OptionsWidget):
  245. @classmethod
  246. def widget(cls, field, value, size=5, **attributes):
  247. """
  248. generates a SELECT tag, including OPTIONs (multiple options allowed)
  249. see also: :meth:`FormWidget.widget`
  250. :param size: optional param (default=5) to indicate how many rows must
  251. be shown
  252. """
  253. attributes.update(_size=size, _multiple=True)
  254. return OptionsWidget.widget(field, value, **attributes)
  255. class RadioWidget(OptionsWidget):
  256. @classmethod
  257. def widget(cls, field, value, **attributes):
  258. """
  259. generates a TABLE tag, including INPUT radios (only 1 option allowed)
  260. see also: :meth:`FormWidget.widget`
  261. """
  262. if isinstance(value, (list,tuple)):
  263. value = str(value[0])
  264. else:
  265. value = str(value)
  266. attr = cls._attributes(field, {}, **attributes)
  267. attr['_class'] = attr.get('_class', 'web2py_radiowidget')
  268. requires = field.requires
  269. if not isinstance(requires, (list, tuple)):
  270. requires = [requires]
  271. if requires:
  272. if hasattr(requires[0], 'options'):
  273. options = requires[0].options()
  274. else:
  275. raise SyntaxError('widget cannot determine options of %s'
  276. % field)
  277. options = [(k, v) for k, v in options if str(v)]
  278. opts = []
  279. cols = attributes.get('cols', 1)
  280. totals = len(options)
  281. mods = totals % cols
  282. rows = totals / cols
  283. if mods:
  284. rows += 1
  285. #widget style
  286. wrappers = dict(
  287. table=(TABLE, TR, TD),
  288. ul=(DIV, UL, LI),
  289. divs=(CAT, DIV, DIV)
  290. )
  291. parent, child, inner = wrappers[attributes.get('style', 'table')]
  292. for r_index in range(rows):
  293. tds = []
  294. for k, v in options[r_index * cols:(r_index + 1) * cols]:
  295. checked = {'_checked': 'checked'} if k == value else {}
  296. tds.append(inner(INPUT(_type='radio',
  297. _id='%s%s' % (field.name, k),
  298. _name=field.name,
  299. requires=attr.get('requires', None),
  300. hideerror=True, _value=k,
  301. value=value,
  302. **checked),
  303. LABEL(v, _for='%s%s' % (field.name, k))))
  304. opts.append(child(tds))
  305. if opts:
  306. opts[-1][0][0]['hideerror'] = False
  307. return parent(*opts, **attr)
  308. class CheckboxesWidget(OptionsWidget):
  309. @classmethod
  310. def widget(cls, field, value, **attributes):
  311. """
  312. generates a TABLE tag, including INPUT checkboxes (multiple allowed)
  313. see also: :meth:`FormWidget.widget`
  314. """
  315. # was values = re.compile('[\w\-:]+').findall(str(value))
  316. if isinstance(value, (list, tuple)):
  317. values = [str(v) for v in value]
  318. else:
  319. values = [str(value)]
  320. attr = cls._attributes(field, {}, **attributes)
  321. attr['_class'] = attr.get('_class', 'web2py_checkboxeswidget')
  322. requires = field.requires
  323. if not isinstance(requires, (list, tuple)):
  324. requires = [requires]
  325. if requires and hasattr(requires[0], 'options'):
  326. options = requires[0].options()
  327. else:
  328. raise SyntaxError('widget cannot determine options of %s'
  329. % field)
  330. options = [(k, v) for k, v in options if k != '']
  331. opts = []
  332. cols = attributes.get('cols', 1)
  333. totals = len(options)
  334. mods = totals % cols
  335. rows = totals / cols
  336. if mods:
  337. rows += 1
  338. #widget style
  339. wrappers = dict(
  340. table=(TABLE, TR, TD),
  341. ul=(DIV, UL, LI),
  342. divs=(CAT, DIV, DIV)
  343. )
  344. parent, child, inner = wrappers[attributes.get('style', 'table')]
  345. for r_index in range(rows):
  346. tds = []
  347. for k, v in options[r_index * cols:(r_index + 1) * cols]:
  348. if k in values:
  349. r_value = k
  350. else:
  351. r_value = []
  352. tds.append(inner(INPUT(_type='checkbox',
  353. _id='%s%s' % (field.name, k),
  354. _name=field.name,
  355. requires=attr.get('requires', None),
  356. hideerror=True, _value=k,
  357. value=r_value),
  358. LABEL(v, _for='%s%s' % (field.name, k))))
  359. opts.append(child(tds))
  360. if opts:
  361. opts.append(
  362. INPUT(requires=attr.get('requires', None),
  363. _style="display:none;",
  364. _disabled="disabled",
  365. _name=field.name,
  366. hideerror=False))
  367. return parent(*opts, **attr)
  368. class PasswordWidget(FormWidget):
  369. _class = 'password'
  370. DEFAULT_PASSWORD_DISPLAY = 8 * ('*')
  371. @classmethod
  372. def widget(cls, field, value, **attributes):
  373. """
  374. generates a INPUT password tag.
  375. If a value is present it will be shown as a number of '*', not related
  376. to the length of the actual value.
  377. see also: :meth:`FormWidget.widget`
  378. """
  379. # detect if attached a IS_STRONG with entropy
  380. default = dict(
  381. _type='password',
  382. _value=(value and cls.DEFAULT_PASSWORD_DISPLAY) or '',
  383. )
  384. attr = cls._attributes(field, default, **attributes)
  385. # deal with entropy check!
  386. requires = field.requires
  387. if not isinstance(requires, (list, tuple)):
  388. requires = [requires]
  389. is_strong = [r for r in requires if isinstance(r, IS_STRONG)]
  390. if is_strong:
  391. attr['_data-w2p_entropy'] = is_strong[0].entropy if is_strong[0].entropy else "null"
  392. # end entropy check
  393. output = INPUT(**attr)
  394. return output
  395. class UploadWidget(FormWidget):
  396. _class = 'upload'
  397. DEFAULT_WIDTH = '150px'
  398. ID_DELETE_SUFFIX = '__delete'
  399. GENERIC_DESCRIPTION = 'file'
  400. DELETE_FILE = 'delete'
  401. @classmethod
  402. def widget(cls, field, value, download_url=None, **attributes):
  403. """
  404. generates a INPUT file tag.
  405. Optionally provides an A link to the file, including a checkbox so
  406. the file can be deleted.
  407. All is wrapped in a DIV.
  408. see also: :meth:`FormWidget.widget`
  409. :param download_url: Optional URL to link to the file (default = None)
  410. """
  411. default = dict(_type='file',)
  412. attr = cls._attributes(field, default, **attributes)
  413. inp = INPUT(**attr)
  414. if download_url and value:
  415. if callable(download_url):
  416. url = download_url(value)
  417. else:
  418. url = download_url + '/' + value
  419. (br, image) = ('', '')
  420. if UploadWidget.is_image(value):
  421. br = BR()
  422. image = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
  423. requires = attr["requires"]
  424. if requires == [] or isinstance(requires, IS_EMPTY_OR):
  425. inp = DIV(inp,
  426. SPAN('[',
  427. A(current.T(
  428. UploadWidget.GENERIC_DESCRIPTION), _href=url),
  429. '|',
  430. INPUT(_type='checkbox',
  431. _name=field.name + cls.ID_DELETE_SUFFIX,
  432. _id=field.name + cls.ID_DELETE_SUFFIX),
  433. LABEL(current.T(cls.DELETE_FILE),
  434. _for=field.name + cls.ID_DELETE_SUFFIX,
  435. _style='display:inline'),
  436. ']', _style='white-space:nowrap'),
  437. br, image)
  438. else:
  439. inp = DIV(inp,
  440. SPAN('[',
  441. A(cls.GENERIC_DESCRIPTION, _href=url),
  442. ']', _style='white-space:nowrap'),
  443. br, image)
  444. return inp
  445. @classmethod
  446. def represent(cls, field, value, download_url=None):
  447. """
  448. how to represent the file:
  449. - with download url and if it is an image: <A href=...><IMG ...></A>
  450. - otherwise with download url: <A href=...>file</A>
  451. - otherwise: file
  452. :param field: the field
  453. :param value: the field value
  454. :param download_url: url for the file download (default = None)
  455. """
  456. inp = cls.GENERIC_DESCRIPTION
  457. if download_url and value:
  458. if callable(download_url):
  459. url = download_url(value)
  460. else:
  461. url = download_url + '/' + value
  462. if cls.is_image(value):
  463. inp = IMG(_src=url, _width=cls.DEFAULT_WIDTH)
  464. inp = A(inp, _href=url)
  465. return inp
  466. @staticmethod
  467. def is_image(value):
  468. """
  469. Tries to check if the filename provided references to an image
  470. Checking is based on filename extension. Currently recognized:
  471. gif, png, jp(e)g, bmp
  472. :param value: filename
  473. """
  474. extension = value.split('.')[-1].lower()
  475. if extension in ['gif', 'png', 'jpg', 'jpeg', 'bmp']:
  476. return True
  477. return False
  478. class AutocompleteWidget(object):
  479. _class = 'string'
  480. def __init__(self, request, field, id_field=None, db=None,
  481. orderby=None, limitby=(0, 10), distinct=False,
  482. keyword='_autocomplete_%(tablename)s_%(fieldname)s',
  483. min_length=2, help_fields=None, help_string=None):
  484. self.help_fields = help_fields or []
  485. self.help_string = help_string
  486. if self.help_fields and not self.help_string:
  487. self.help_string = ' '.join('%%(%s)s'%f.name
  488. for f in self.help_fields)
  489. self.request = request
  490. self.keyword = keyword % dict(tablename=field.tablename,
  491. fieldname=field.name)
  492. self.db = db or field._db
  493. self.orderby = orderby
  494. self.limitby = limitby
  495. self.distinct = distinct
  496. self.min_length = min_length
  497. self.fields = [field]
  498. if id_field:
  499. self.is_reference = True
  500. self.fields.append(id_field)
  501. else:
  502. self.is_reference = False
  503. if hasattr(request, 'application'):
  504. self.url = URL(args=request.args)
  505. self.callback()
  506. else:
  507. self.url = request
  508. def callback(self):
  509. if self.keyword in self.request.vars:
  510. field = self.fields[0]
  511. if is_gae:
  512. 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))
  513. else:
  514. 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))
  515. if rows:
  516. if self.is_reference:
  517. id_field = self.fields[1]
  518. if self.help_fields:
  519. options = [OPTION(
  520. self.help_string % dict([(h.name, s[h.name]) for h in self.fields[:1] + self.help_fields]),
  521. _value=s[id_field.name], _selected=(k == 0)) for k, s in enumerate(rows)]
  522. else:
  523. options = [OPTION(
  524. s[field.name], _value=s[id_field.name],
  525. _selected=(k == 0)) for k, s in enumerate(rows)]
  526. raise HTTP(
  527. 200, SELECT(_id=self.keyword, _class='autocomplete',
  528. _size=len(rows), _multiple=(len(rows) == 1),
  529. *options).xml())
  530. else:
  531. raise HTTP(
  532. 200, SELECT(_id=self.keyword, _class='autocomplete',
  533. _size=len(rows), _multiple=(len(rows) == 1),
  534. *[OPTION(s[field.name],
  535. _selected=(k == 0))
  536. for k, s in enumerate(rows)]).xml())
  537. else:
  538. raise HTTP(200, '')
  539. def __call__(self, field, value, **attributes):
  540. default = dict(
  541. _type='text',
  542. value=(not value is None and str(value)) or '',
  543. )
  544. attr = StringWidget._attributes(field, default, **attributes)
  545. div_id = self.keyword + '_div'
  546. attr['_autocomplete'] = 'off'
  547. if self.is_reference:
  548. key2 = self.keyword + '_aux'
  549. key3 = self.keyword + '_auto'
  550. attr['_class'] = 'string'
  551. name = attr['_name']
  552. if 'requires' in attr:
  553. del attr['requires']
  554. attr['_name'] = key2
  555. value = attr['value']
  556. record = self.db(
  557. self.fields[1] == value).select(self.fields[0]).first()
  558. attr['value'] = record and record[self.fields[0].name]
  559. attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
  560. dict(div_id=div_id, u='F' + self.keyword)
  561. 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');" % \
  562. dict(url=self.url, min_length=self.min_length,
  563. key=self.keyword, id=attr['_id'], key2=key2, key3=key3,
  564. name=name, div_id=div_id, u='F' + self.keyword)
  565. if self.min_length == 0:
  566. attr['_onfocus'] = attr['_onkeyup']
  567. return TAG[''](INPUT(**attr), INPUT(_type='hidden', _id=key3, _value=value,
  568. _name=name, requires=field.requires),
  569. DIV(_id=div_id, _style='position:absolute;'))
  570. else:
  571. attr['_name'] = field.name
  572. attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \
  573. dict(div_id=div_id, u='F' + self.keyword)
  574. 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');" % \
  575. dict(url=self.url, min_length=self.min_length,
  576. key=self.keyword, id=attr['_id'], div_id=div_id, u='F' + self.keyword)
  577. if self.min_length == 0:
  578. attr['_onfocus'] = attr['_onkeyup']
  579. return TAG[''](INPUT(**attr), DIV(_id=div_id, _style='position:absolute;'))
  580. def formstyle_table3cols(form, fields):
  581. ''' 3 column table - default '''
  582. table = TABLE()
  583. for id, label, controls, help in fields:
  584. _help = TD(help, _class='w2p_fc')
  585. _controls = TD(controls, _class='w2p_fw')
  586. _label = TD(label, _class='w2p_fl')
  587. table.append(TR(_label, _controls, _help, _id=id))
  588. return table
  589. def formstyle_table2cols(form, fields):
  590. ''' 2 column table '''
  591. table = TABLE()
  592. for id, label, controls, help in fields:
  593. _help = TD(help, _class='w2p_fc', _width='50%')
  594. _controls = TD(controls, _class='w2p_fw', _colspan='2')
  595. _label = TD(label, _class='w2p_fl', _width='50%')
  596. table.append(TR(_label, _help, _id=id + '1', _class='even'))
  597. table.append(TR(_controls, _id=id + '2', _class='odd'))
  598. return table
  599. def formstyle_divs(form, fields):
  600. ''' divs only '''
  601. table = FIELDSET()
  602. for id, label, controls, help in fields:
  603. _help = DIV(help, _class='w2p_fc')
  604. _controls = DIV(controls, _class='w2p_fw')
  605. _label = DIV(label, _class='w2p_fl')
  606. table.append(DIV(_label, _controls, _help, _id=id))
  607. return table
  608. def formstyle_inline(form, fields):
  609. ''' divs only '''
  610. if len(fields) != 2:
  611. raise RuntimeError("Not possible")
  612. id, label, controls, help = fields[0]
  613. submit_button = fields[1][2]
  614. return CAT(DIV(controls, _style='display:inline'),
  615. submit_button)
  616. def formstyle_ul(form, fields):
  617. ''' unordered list '''
  618. table = UL()
  619. for id, label, controls, help in fields:
  620. _help = DIV(help, _class='w2p_fc')
  621. _controls = DIV(controls, _class='w2p_fw')
  622. _label = DIV(label, _class='w2p_fl')
  623. table.append(LI(_label, _controls, _help, _id=id))
  624. return table
  625. def formstyle_bootstrap(form, fields):
  626. ''' bootstrap format form layout '''
  627. form.add_class('form-horizontal')
  628. parent = FIELDSET()
  629. for id, label, controls, help in fields:
  630. # wrappers
  631. _help = SPAN(help, _class='help-block')
  632. # embed _help into _controls
  633. _controls = DIV(controls, _help, _class='controls')
  634. # submit unflag by default
  635. _submit = False
  636. if isinstance(controls, INPUT):
  637. controls.add_class('span4')
  638. if controls['_type'] == 'submit':
  639. # flag submit button
  640. _submit = True
  641. controls['_class'] = 'btn btn-primary'
  642. if controls['_type'] == 'file':
  643. controls['_class'] = 'input-file'
  644. # For password fields, which are wrapped in a CAT object.
  645. if isinstance(controls, CAT) and isinstance(controls[0], INPUT):
  646. controls[0].add_class('span4')
  647. if isinstance(controls, SELECT):
  648. controls.add_class('span4')
  649. if isinstance(controls, TEXTAREA):
  650. controls.add_class('span4')
  651. if isinstance(label, LABEL):
  652. label['_class'] = 'control-label'
  653. if _submit:
  654. # submit button has unwrapped label and controls, different class
  655. parent.append(DIV(label, controls, _class='form-actions', _id=id))
  656. # unflag submit (possible side effect)
  657. _submit = False
  658. else:
  659. # unwrapped label
  660. parent.append(DIV(label, _controls, _class='control-group', _id=id))
  661. return parent
  662. class SQLFORM(FORM):
  663. """
  664. SQLFORM is used to map a table (and a current record) into an HTML form
  665. given a SQLTable stored in db.table
  666. generates an insert form::
  667. SQLFORM(db.table)
  668. generates an update form::
  669. record=db.table[some_id]
  670. SQLFORM(db.table, record)
  671. generates an update with a delete button::
  672. SQLFORM(db.table, record, deletable=True)
  673. if record is an int::
  674. record=db.table[record]
  675. optional arguments:
  676. :param fields: a list of fields that should be placed in the form,
  677. default is all.
  678. :param labels: a dictionary with labels for each field, keys are the field
  679. names.
  680. :param col3: a dictionary with content for an optional third column
  681. (right of each field). keys are field names.
  682. :param linkto: the URL of a controller/function to access referencedby
  683. records
  684. see controller appadmin.py for examples
  685. :param upload: the URL of a controller/function to download an uploaded file
  686. see controller appadmin.py for examples
  687. any named optional attribute is passed to the <form> tag
  688. for example _class, _id, _style, _action, _method, etc.
  689. """
  690. # usability improvements proposal by fpp - 4 May 2008 :
  691. # - correct labels (for points to field id, not field name)
  692. # - add label for delete checkbox
  693. # - add translatable label for record ID
  694. # - add third column to right of fields, populated from the col3 dict
  695. widgets = Storage(dict(
  696. string=StringWidget,
  697. text=TextWidget,
  698. json=JSONWidget,
  699. password=PasswordWidget,
  700. integer=IntegerWidget,
  701. double=DoubleWidget,
  702. decimal=DecimalWidget,
  703. time=TimeWidget,
  704. date=DateWidget,
  705. datetime=DatetimeWidget,
  706. upload=UploadWidget,
  707. boolean=BooleanWidget,
  708. blob=None,
  709. options=OptionsWidget,
  710. multiple=MultipleOptionsWidget,
  711. radio=RadioWidget,
  712. checkboxes=CheckboxesWidget,
  713. autocomplete=AutocompleteWidget,
  714. list=ListWidget,
  715. ))
  716. formstyles = Storage(dict(
  717. table3cols=formstyle_table3cols,
  718. table2cols=formstyle_table2cols,
  719. divs=formstyle_divs,
  720. ul=formstyle_ul,
  721. bootstrap=formstyle_bootstrap,
  722. inline=formstyle_inline,
  723. ))
  724. FIELDNAME_REQUEST_DELETE = 'delete_this_record'
  725. FIELDKEY_DELETE_RECORD = 'delete_record'
  726. ID_LABEL_SUFFIX = '__label'
  727. ID_ROW_SUFFIX = '__row'
  728. def assert_status(self, status, request_vars):
  729. if not status and self.record and self.errors:
  730. ### if there are errors in update mode
  731. # and some errors refers to an already uploaded file
  732. # delete error if
  733. # - user not trying to upload a new file
  734. # - there is existing file and user is not trying to delete it
  735. # this is because removing the file may not pass validation
  736. for key in self.errors.keys():
  737. if key in self.table \
  738. and self.table[key].type == 'upload' \
  739. and request_vars.get(key, None) in (None, '') \
  740. and self.record[key] \
  741. and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars:
  742. del self.errors[key]
  743. if not self.errors:
  744. status = True
  745. return status
  746. def __init__(
  747. self,
  748. table,
  749. record=None,
  750. deletable=False,
  751. linkto=None,
  752. upload=None,
  753. fields=None,
  754. labels=None,
  755. col3={},
  756. submit_button='Submit',
  757. delete_label='Check to delete',
  758. showid=True,
  759. readonly=False,
  760. comments=True,
  761. keepopts=[],
  762. ignore_rw=False,
  763. record_id=None,
  764. formstyle='table3cols',
  765. buttons=['submit'],
  766. separator=': ',
  767. **attributes
  768. ):
  769. """
  770. SQLFORM(db.table,
  771. record=None,
  772. fields=['name'],
  773. labels={'name': 'Your name'},
  774. linkto=URL(f='table/db/')
  775. """
  776. T = current.T
  777. self.ignore_rw = ignore_rw
  778. self.formstyle = formstyle
  779. self.readonly = readonly
  780. # Default dbio setting
  781. self.detect_record_change = None
  782. nbsp = XML('&nbsp;') # Firefox2 does not display fields with blanks
  783. FORM.__init__(self, *[], **attributes)
  784. ofields = fields
  785. keyed = hasattr(table, '_primarykey') # for backward compatibility
  786. # if no fields are provided, build it from the provided table
  787. # will only use writable or readable fields, unless forced to ignore
  788. if fields is None:
  789. fields = [f.name for f in table if
  790. (ignore_rw or f.writable or f.readable) and
  791. (readonly or not f.compute)]
  792. self.fields = fields
  793. # make sure we have an id
  794. if self.fields[0] != table.fields[0] and \
  795. isinstance(table, Table) and not keyed:
  796. self.fields.insert(0, table.fields[0])
  797. self.table = table
  798. # try to retrieve the indicated record using its id
  799. # otherwise ignore it
  800. if record and isinstance(record, (int, long, str, unicode)):
  801. if not str(record).isdigit():
  802. raise HTTP(404, "Object not found")
  803. record = table._db(table._id == record).select().first()
  804. if not record:
  805. raise HTTP(404, "Object not found")
  806. self.record = record
  807. self.record_id = record_id
  808. if keyed:
  809. self.record_id = dict([(k, record and str(record[k]) or None)
  810. for k in table._primarykey])
  811. self.field_parent = {}
  812. xfields = []
  813. self.fields = fields
  814. self.custom = Storage()
  815. self.custom.dspval = Storage()
  816. self.custom.inpval = Storage()
  817. self.custom.label = Storage()
  818. self.custom.comment = Storage()
  819. self.custom.widget = Storage()
  820. self.custom.linkto = Storage()
  821. # default id field name
  822. if not keyed:
  823. self.id_field_name = table._id.name
  824. else:
  825. self.id_field_name = table._primarykey[0] # only works if one key
  826. sep = separator or ''
  827. for fieldname in self.fields:
  828. if fieldname.find('.') >= 0:
  829. continue
  830. field = self.table[fieldname]
  831. comment = None
  832. if comments:
  833. comment = col3.get(fieldname, field.comment)
  834. if comment is None:
  835. comment = ''
  836. self.custom.comment[fieldname] = comment
  837. if not labels is None and fieldname in labels:
  838. label = labels[fieldname]
  839. else:
  840. label = field.label
  841. self.custom.label[fieldname] = label
  842. field_id = '%s_%s' % (table._tablename, fieldname)
  843. label = LABEL(label, label and sep, _for=field_id,
  844. _id=field_id + SQLFORM.ID_LABEL_SUFFIX)
  845. row_id = field_id + SQLFORM.ID_ROW_SUFFIX
  846. if field.type == 'id':
  847. self.custom.dspval.id = nbsp
  848. self.custom.inpval.id = ''
  849. widget = ''
  850. # store the id field name (for legacy databases)
  851. self.id_field_name = field.name
  852. if record:
  853. if showid and field.name in record and field.readable:
  854. v = record[field.name]
  855. widget = SPAN(v, _id=field_id)
  856. self.custom.dspval.id = str(v)
  857. xfields.append((row_id, label, widget, comment))
  858. self.record_id = str(record[field.name])
  859. self.custom.widget.id = widget
  860. continue
  861. if readonly and not ignore_rw and not field.readable:
  862. continue
  863. if record:
  864. default = record[fieldname]
  865. else:
  866. default = field.default
  867. if isinstance(default, CALLABLETYPES):
  868. default = default()
  869. cond = readonly or \
  870. (not ignore_rw and not field.writable and field.readable)
  871. if default is not None and not cond:
  872. default = field.formatter(default)
  873. dspval = default
  874. inpval = default
  875. if cond:
  876. # ## if field.represent is available else
  877. # ## ignore blob and preview uploaded images
  878. # ## format everything else
  879. if field.represent:
  880. inp = represent(field, default, record)
  881. elif field.type in ['blob']:
  882. continue
  883. elif field.type == 'upload':
  884. inp = UploadWidget.represent(field, default, upload)
  885. elif field.type == 'boolean':
  886. inp = self.widgets.boolean.widget(
  887. field, default, _disabled=True)
  888. else:
  889. inp = field.formatter(default)
  890. elif field.type == 'upload':
  891. if field.widget:
  892. inp = field.widget(field, default, upload)
  893. else:
  894. inp = self.widgets.upload.widget(field, default, upload)
  895. elif field.widget:
  896. inp = field.widget(field, default)
  897. elif field.type == 'boolean':
  898. inp = self.widgets.boolean.widget(field, default)
  899. if default:
  900. inpval = 'checked'
  901. else:
  902. inpval = ''
  903. elif OptionsWidget.has_options(field):
  904. if not field.requires.multiple:
  905. inp = self.widgets.options.widget(field, default)
  906. else:
  907. inp = self.widgets.multiple.widget(field, default)
  908. if fieldname in keepopts:
  909. inpval = TAG[''](*inp.components)
  910. elif field.type.startswith('list:'):
  911. inp = self.widgets.list.widget(field, default)
  912. elif field.type == 'text':
  913. inp = self.widgets.text.widget(field, default)
  914. elif field.type == 'password':
  915. inp = self.widgets.password.widget(field, default)
  916. if self.record:
  917. dspval = PasswordWidget.DEFAULT_PASSWORD_DISPLAY
  918. else:
  919. dspval = ''
  920. elif field.type == 'blob':
  921. continue
  922. else:
  923. field_type = widget_class.match(str(field.type)).group()
  924. field_type = field_type in self.widgets and field_type or 'string'
  925. inp = self.widgets[field_type].widget(field, default)
  926. xfields.append((row_id, label, inp, comment))
  927. self.custom.dspval[fieldname] = dspval if (dspval is not None) else nbsp
  928. self.custom.inpval[
  929. fieldname] = inpval if not inpval is None else ''
  930. self.custom.widget[fieldname] = inp
  931. # if a record is provided and found, as is linkto
  932. # build a link
  933. if record and linkto:
  934. db = linkto.split('/')[-1]
  935. for rfld in table._referenced_by:
  936. if keyed:
  937. query = urllib.quote('%s.%s==%s' % (
  938. db, rfld, record[rfld.type[10:].split('.')[1]]))
  939. else:
  940. query = urllib.quote(
  941. '%s.%s==%s' % (db, rfld, record[self.id_field_name]))
  942. lname = olname = '%s.%s' % (rfld.tablename, rfld.name)
  943. if ofields and not olname in ofields:
  944. continue
  945. if labels and lname in labels:
  946. lname = labels[lname]
  947. widget = A(lname,
  948. _class='reference',
  949. _href='%s/%s?query=%s' % (linkto, rfld.tablename, query))
  950. xfields.append(
  951. (olname.replace('.', '__') + SQLFORM.ID_ROW_SUFFIX,
  952. '', widget, col3.get(olname, '')))
  953. self.custom.linkto[olname.replace('.', '__')] = widget
  954. # </block>
  955. # when deletable, add delete? checkbox
  956. self.custom.delete = self.custom.deletable = ''
  957. if record and deletable:
  958. #add secondary css class for cascade delete warning
  959. css = 'delete'
  960. for f in self.table.fields:
  961. on_del = self.table[f].ondelete
  962. if isinstance(on_del,str) and 'cascade' in on_del.lower():
  963. css += ' cascade_delete'
  964. break
  965. widget = INPUT(_type='checkbox',
  966. _class=css,
  967. _id=self.FIELDKEY_DELETE_RECORD,
  968. _name=self.FIELDNAME_REQUEST_DELETE,
  969. )
  970. xfields.append(
  971. (self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_ROW_SUFFIX,
  972. LABEL(
  973. T(delete_label), separator,
  974. _for=self.FIELDKEY_DELETE_RECORD,
  975. _id=self.FIELDKEY_DELETE_RECORD + \
  976. SQLFORM.ID_LABEL_SUFFIX),
  977. widget,
  978. col3.get(self.FIELDKEY_DELETE_RECORD, '')))
  979. self.custom.delete = self.custom.deletable = widget
  980. # when writable, add submit button
  981. self.custom.submit = ''
  982. if not readonly:
  983. if 'submit' in buttons:
  984. widget = self.custom.submit = INPUT(_type='submit',
  985. _value=T(submit_button))
  986. elif buttons:
  987. widget = self.custom.submit = DIV(*buttons)
  988. if self.custom.submit:
  989. xfields.append(('submit_record' + SQLFORM.ID_ROW_SUFFIX,
  990. '', widget, col3.get('submit_button', '')))
  991. # if a record is provided and found
  992. # make sure it's id is stored in the form
  993. if record:
  994. if not self['hidden']:
  995. self['hidden'] = {}
  996. if not keyed:
  997. self['hidden']['id'] = record[table._id.name]
  998. (begin, end) = self._xml()
  999. self.custom.begin = XML("<%s %s>" % (self.tag, begin))
  1000. self.custom.end = XML("%s</%s>" % (end, self.tag))
  1001. table = self.createform(xfields)
  1002. self.components = [table]
  1003. def createform(self, xfields):
  1004. formstyle = self.formstyle
  1005. if isinstance(formstyle, basestring):
  1006. if formstyle in SQLFORM.formstyles:
  1007. formstyle = SQLFORM.formstyles[formstyle]
  1008. else:
  1009. raise RuntimeError('formstyle not found')
  1010. if callable(formstyle):
  1011. # backward compatibility, 4 argument function is the old style
  1012. args, varargs, keywords, defaults = inspect.getargspec(formstyle)
  1013. if defaults and len(args) - len(defaults) == 4 or len(args) == 4:
  1014. table = TABLE()
  1015. for id, a, b, c in xfields:
  1016. newrows = formstyle(id, a, b, c)
  1017. self.field_parent[id] = getattr(b, 'parent', None) \
  1018. if isinstance(b,XmlComponent) else None
  1019. if type(newrows).__name__ != "tuple":
  1020. newrows = [newrows]
  1021. for newrow in newrows:
  1022. table.append(newrow)
  1023. else:
  1024. table = formstyle(self, xfields)
  1025. for id, a, b, c in xfields:
  1026. self.field_parent[id] = getattr(b, 'parent', None) \
  1027. if isinstance(b,XmlComponent) else None
  1028. else:
  1029. raise RuntimeError('formstyle not supported')
  1030. return table
  1031. def accepts(
  1032. self,
  1033. request_vars,
  1034. session=None,
  1035. formname='%(tablename)s/%(record_id)s',
  1036. keepvalues=None,
  1037. onvalidation=None,
  1038. dbio=True,
  1039. hideerror=False,
  1040. detect_record_change=False,
  1041. **kwargs
  1042. ):
  1043. """
  1044. similar FORM.accepts but also does insert, update or delete in DAL.
  1045. but if detect_record_change == True than:
  1046. form.record_changed = False (record is properly validated/submitted)
  1047. form.record_changed = True (record cannot be submitted because changed)
  1048. elseif detect_record_change == False than:
  1049. form.record_changed = None
  1050. """
  1051. if keepvalues is None:
  1052. keepvalues = True if self.record else False
  1053. if self.readonly:
  1054. return False
  1055. if request_vars.__class__.__name__ == 'Request':
  1056. request_vars = request_vars.post_vars
  1057. keyed = hasattr(self.table, '_primarykey')
  1058. # implement logic to detect whether record exist but has been modified
  1059. # server side
  1060. self.record_changed = None
  1061. self.detect_record_change = detect_record_change
  1062. if self.detect_record_change:
  1063. if self.record:
  1064. self.record_changed = False
  1065. serialized = '|'.join(
  1066. str(self.record[k]) for k in self.table.fields())
  1067. self.record_hash = md5_hash(serialized)
  1068. # logic to deal with record_id for keyed tables
  1069. if self.record:
  1070. if keyed:
  1071. formname_id = '.'.join(str(self.record[k])
  1072. for k in self.table._primarykey
  1073. if hasattr(self.record, k))
  1074. record_id = dict((k, request_vars.get(k, None))
  1075. for k in self.table._primarykey)
  1076. else:
  1077. (formname_id, record_id) = (self.record[self.id_field_name],
  1078. request_vars.get('id', None))
  1079. keepvalues = True
  1080. else:
  1081. if keyed:
  1082. formname_id = 'create'
  1083. record_id = dict([(k, None) for k in self.table._primarykey])
  1084. else:
  1085. (formname_id, record_id) = ('create', None)
  1086. if not keyed and isinstance(record_id, (list, tuple)):
  1087. record_id = record_id[0]
  1088. if formname:
  1089. formname = formname % dict(tablename=self.table._tablename,
  1090. record_id=formname_id)
  1091. # ## THIS IS FOR UNIQUE RECORDS, read IS_NOT_IN_DB
  1092. for fieldname in self.fields:
  1093. field = self.table[fieldname]
  1094. requires = field.requires or []
  1095. if not isinstance(requires, (list, tuple)):
  1096. requires = [requires]
  1097. [item.set_self_id(self.record_id) for item in requires
  1098. if hasattr(item, 'set_self_id') and self.record_id]
  1099. # ## END
  1100. fields = {}
  1101. for key in self.vars:
  1102. fields[key] = self.vars[key]
  1103. ret = FORM.accepts(
  1104. self,
  1105. request_vars,
  1106. session,
  1107. formname,
  1108. keepvalues,
  1109. onvalidation,
  1110. hideerror=hideerror,
  1111. **kwargs
  1112. )
  1113. self.deleted = \
  1114. request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
  1115. self.custom.end = TAG[''](self.hidden_fields(), self.custom.end)
  1116. auch = record_id and self.errors and self.deleted
  1117. if self.record_changed and self.detect_record_change:
  1118. message_onchange = \
  1119. kwargs.setdefault("message_onchange",
  1120. current.T("A record change was detected. " +
  1121. "Consecutive update self-submissions " +
  1122. "are not allowed. Try re-submitting or " +
  1123. "refreshing the form page."))
  1124. if message_onchange is not None:
  1125. current.response.flash = message_onchange
  1126. return ret
  1127. elif (not ret) and (not auch):
  1128. # auch is true when user tries to delete a record
  1129. # that does not pass validation, yet it should be deleted
  1130. for fieldname in self.fields:
  1131. field = self.table[fieldname]
  1132. ### this is a workaround! widgets should always have default not None!
  1133. if not field.widget and field.type.startswith('list:') and \
  1134. not OptionsWidget.has_options(field):
  1135. field.widget = self.widgets.list.widget
  1136. if field.widget and fieldname in request_vars:
  1137. if fieldname in self.request_vars:
  1138. value = self.request_vars[fieldname]
  1139. elif self.record:
  1140. value = self.record[fieldname]
  1141. else:
  1142. value = self.table[fieldname].default
  1143. if field.type.startswith('list:') and isinstance(v

Large files files are truncated, but you can click here to view the full file