PageRenderTime 80ms CodeModel.GetById 22ms 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
  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(value, str):
  1144. value = [value]
  1145. row_id = '%s_%s%s' % (
  1146. self.table, fieldname, SQLFORM.ID_ROW_SUFFIX)
  1147. widget = field.widget(field, value)
  1148. parent = self.field_parent[row_id]
  1149. if parent:
  1150. parent.components = [widget]
  1151. if self.errors.get(fieldname):
  1152. parent._traverse(False, hideerror)
  1153. self.custom.widget[fieldname] = widget
  1154. self.accepted = ret
  1155. return ret
  1156. if record_id and str(record_id) != str(self.record_id):
  1157. raise SyntaxError('user is tampering with form\'s record_id: '
  1158. '%s != %s' % (record_id, self.record_id))
  1159. if record_id and dbio and not keyed:
  1160. self.vars.id = self.record[self.id_field_name]
  1161. if self.deleted and self.custom.deletable:
  1162. if dbio:
  1163. if keyed:
  1164. qry = reduce(lambda x, y: x & y,
  1165. [self.table[k] == record_id[k]
  1166. for k in self.table._primarykey])
  1167. else:
  1168. qry = self.table._id == self.record[self.id_field_name]
  1169. self.table._db(qry).delete()
  1170. self.errors.clear()
  1171. for component in self.elements('input, select, textarea'):
  1172. component['_disabled'] = True
  1173. self.accepted = True
  1174. return True
  1175. for fieldname in self.fields:
  1176. if not fieldname in self.table.fields:
  1177. continue
  1178. if not self.ignore_rw and not self.table[fieldname].writable:
  1179. ### this happens because FORM has no knowledge of writable
  1180. ### and thinks that a missing boolean field is a None
  1181. if self.table[fieldname].type == 'boolean' and \
  1182. self.vars.get(fieldname, True) is None:
  1183. del self.vars[fieldname]
  1184. continue
  1185. field = self.table[fieldname]
  1186. if field.type == 'id':
  1187. continue
  1188. if field.type == 'boolean':
  1189. if self.vars.get(fieldname, False):
  1190. self.vars[fieldname] = fields[fieldname] = True
  1191. else:
  1192. self.vars[fieldname] = fields[fieldname] = False
  1193. elif field.type == 'password' and self.record\
  1194. and request_vars.get(fieldname, None) == \
  1195. PasswordWidget.DEFAULT_PASSWORD_DISPLAY:
  1196. continue # do not update if password was not changed
  1197. elif field.type == 'upload':
  1198. f = self.vars[fieldname]
  1199. fd = '%s__delete' % fieldname
  1200. if f == '' or f is None:
  1201. if self.vars.get(fd, False):
  1202. f = self.table[fieldname].default or ''
  1203. fields[fieldname] = f
  1204. elif self.record:
  1205. if self.record[fieldname]:
  1206. fields[fieldname] = self.record[fieldname]
  1207. else:
  1208. f = self.table[fieldname].default or ''
  1209. fields[fieldname] = f
  1210. else:
  1211. f = self.table[fieldname].default or ''
  1212. fields[fieldname] = f
  1213. self.vars[fieldname] = fields[fieldname]
  1214. if not f:
  1215. continue
  1216. else:
  1217. f = os.path.join(
  1218. current.request.folder,
  1219. os.path.normpath(f))
  1220. source_file = open(f, 'rb')
  1221. original_filename = os.path.split(f)[1]
  1222. elif hasattr(f, 'file'):
  1223. (source_file, original_filename) = (f.file, f.filename)
  1224. elif isinstance(f, (str, unicode)):
  1225. ### do not know why this happens, it should not
  1226. (source_file, original_filename) = \
  1227. (cStringIO.StringIO(f), 'file.txt')
  1228. else:
  1229. # this should never happen, why does it happen?
  1230. print 'f=',repr(f)
  1231. continue
  1232. newfilename = field.store(source_file, original_filename,
  1233. field.uploadfolder)
  1234. # this line was for backward compatibility but problematic
  1235. # self.vars['%s_newfilename' % fieldname] = newfilename
  1236. fields[fieldname] = newfilename
  1237. if isinstance(field.uploadfield, str):
  1238. fields[field.uploadfield] = source_file.read()
  1239. # proposed by Hamdy (accept?) do we need fields at this point?
  1240. self.vars[fieldname] = fields[fieldname]
  1241. continue
  1242. elif fieldname in self.vars:
  1243. fields[fieldname] = self.vars[fieldname]
  1244. elif field.default is None and field.type != 'blob':
  1245. self.errors[fieldname] = 'no data'
  1246. self.accepted = False
  1247. return False
  1248. value = fields.get(fieldname, None)
  1249. if field.type == 'list:string':
  1250. if not isinstance(value, (tuple, list)):
  1251. fields[fieldname] = value and [value] or []
  1252. elif isinstance(field.type, str) and field.type.startswith('list:'):
  1253. if not isinstance(value, list):
  1254. fields[fieldname] = [safe_int(
  1255. x) for x in (value and [value] or [])]
  1256. elif field.type == 'integer':
  1257. if not value is None:
  1258. fields[fieldname] = safe_int(value)
  1259. elif field.type.startswith('reference'):
  1260. if not value is None and isinstance(self.table, Table) and not keyed:
  1261. fields[fieldname] = safe_int(value)
  1262. elif field.type == 'double':
  1263. if not value is None:
  1264. fields[fieldname] = safe_float(value)
  1265. for fieldname in self.vars:
  1266. if fieldname != 'id' and fieldname in self.table.fields\
  1267. and not fieldname in fields and not fieldname\
  1268. in request_vars:
  1269. fields[fieldname] = self.vars[fieldname]
  1270. if dbio:
  1271. if 'delete_this_record' in fields:
  1272. # this should never happen but seems to happen to some
  1273. del fields['delete_this_record']
  1274. for field in self.table:
  1275. if not field.name in fields and field.writable is False \
  1276. and field.update is None and field.compute is None:
  1277. if record_id and self.record:
  1278. fields[field.name] = self.record[field.name]
  1279. elif not self.table[field.name].default is None:
  1280. fields[field.name] = self.table[field.name].default
  1281. if keyed:
  1282. if reduce(lambda x, y: x and y, record_id.values()): # if record_id
  1283. if fields:
  1284. qry = reduce(lambda x, y: x & y,
  1285. [self.table[k] == self.record[k] for k in self.table._primarykey])
  1286. self.table._db(qry).update(**fields)
  1287. else:
  1288. pk = self.table.insert(**fields)
  1289. if pk:
  1290. self.vars.update(pk)
  1291. else:
  1292. ret = False
  1293. else:
  1294. if record_id:
  1295. self.vars.id = self.record[self.id_field_name]
  1296. if fields:
  1297. self.table._db(self.table._id == self.record[
  1298. self.id_field_name]).update(**fields)
  1299. else:
  1300. self.vars.id = self.table.insert(**fields)
  1301. self.accepted = ret
  1302. return ret
  1303. AUTOTYPES = {
  1304. type(''): ('string', None),
  1305. type(True): ('boolean', None),
  1306. type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
  1307. type(1.0): ('double', IS_FLOAT_IN_RANGE()),
  1308. type([]): ('list:string', None),
  1309. type(datetime.date.today()): ('date', IS_DATE()),
  1310. type(datetime.datetime.today()): ('datetime', IS_DATETIME())
  1311. }
  1312. @staticmethod
  1313. def dictform(dictionary, **kwargs):
  1314. fields = []
  1315. for key, value in sorted(dictionary.items()):
  1316. t, requires = SQLFORM.AUTOTYPES.get(type(value), (None, None))
  1317. if t:
  1318. fields.append(Field(key, t, requires=requires,
  1319. default=value))
  1320. return SQLFORM.factory(*fields, **kwargs)
  1321. @staticmethod
  1322. def smartdictform(session, name, filename=None, query=None, **kwargs):
  1323. import os
  1324. if query:
  1325. session[name] = query.db(query).select().first().as_dict()
  1326. elif os.path.exists(filename):
  1327. env = {'datetime': datetime}
  1328. session[name] = eval(open(filename).read(), {}, env)
  1329. form = SQLFORM.dictform(session[name])
  1330. if form.process().accepted:
  1331. session[name].update(form.vars)
  1332. if query:
  1333. query.db(query).update(**form.vars)
  1334. else:
  1335. open(filename, 'w').write(repr(session[name]))
  1336. return form
  1337. @staticmethod
  1338. def factory(*fields, **attributes):
  1339. """
  1340. generates a SQLFORM for the given fields.
  1341. Internally will build a non-database based data model
  1342. to hold the fields.
  1343. """
  1344. # Define a table name, this way it can be logical to our CSS.
  1345. # And if you switch from using SQLFORM to SQLFORM.factory
  1346. # your same css definitions will still apply.
  1347. table_name = attributes.get('table_name', 'no_table')
  1348. # So it won't interfere with SQLDB.define_table
  1349. if 'table_name' in attributes:
  1350. del attributes['table_name']
  1351. return SQLFORM(DAL(None).define_table(table_name, *fields),
  1352. **attributes)
  1353. @staticmethod
  1354. def build_query(fields, keywords):
  1355. request = current.request
  1356. if isinstance(keywords, (tuple, list)):
  1357. keywords = keywords[0]
  1358. request.vars.keywords = keywords
  1359. key = keywords.strip()
  1360. if key and not ' ' in key and not '"' in key and not "'" in key:
  1361. SEARCHABLE_TYPES = ('string', 'text', 'list:string')
  1362. parts = [field.contains(
  1363. key) for field in fields if field.type in SEARCHABLE_TYPES]
  1364. else:
  1365. parts = None
  1366. if parts:
  1367. return reduce(lambda a, b: a | b, parts)
  1368. else:
  1369. return smart_query(fields, key)
  1370. @staticmethod
  1371. def search_menu(fields,
  1372. search_options=None,
  1373. prefix='w2p'
  1374. ):
  1375. T = current.T
  1376. panel_id='%s_query_panel' % prefix
  1377. fields_id='%s_query_fields' % prefix
  1378. keywords_id='%s_keywords' % prefix
  1379. field_id='%s_field' % prefix
  1380. value_id='%s_value' % prefix
  1381. search_options = search_options or {
  1382. 'string': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'],
  1383. 'text': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'],
  1384. 'date': ['=', '!=', '<', '>', '<=', '>='],
  1385. 'time': ['=', '!=', '<', '>', '<=', '>='],
  1386. 'datetime': ['=', '!=', '<', '>', '<=', '>='],
  1387. 'integer': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'],
  1388. 'double': ['=', '!=', '<', '>', '<=', '>='],
  1389. 'id': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'],
  1390. 'reference': ['=', '!='],
  1391. 'boolean': ['=', '!=']}
  1392. if fields[0]._db._adapter.dbengine == 'google:datastore':
  1393. search_options['string'] = ['=', '!=', '<', '>', '<=', '>=']
  1394. search_options['text'] = ['=', '!=', '<', '>', '<=', '>=']
  1395. search_options['list:string'] = ['contains']
  1396. search_options['list:integer'] = ['contains']
  1397. search_options['list:reference'] = ['contains']
  1398. criteria = []
  1399. selectfields = []
  1400. for field in fields:
  1401. name = str(field).replace('.', '-')
  1402. # treat ftype 'decimal' as 'double'
  1403. # (this fixes problems but needs refactoring!
  1404. ftype = field.type.split(' ')[0]
  1405. if ftype.startswith('decimal'): ftype = 'double'
  1406. elif ftype=='bigint': ftype = 'integer'
  1407. elif ftype.startswith('big-'): ftype = ftype[4:]
  1408. # end
  1409. options = search_options.get(ftype, None)
  1410. if options:
  1411. label = isinstance(
  1412. field.label, str) and T(field.label) or field.label
  1413. selectfields.append(OPTION(label, _value=str(field)))
  1414. operators = SELECT(*[OPTION(T(option), _value=option) for option in options])
  1415. _id = "%s_%s" % (value_id,name)
  1416. if field.type == 'boolean':
  1417. value_input = SQLFORM.widgets.boolean.widget(field,field.default,_id=_id)
  1418. elif field.type == 'double':
  1419. value_input = SQLFORM.widgets.double.widget(field,field.default,_id=_id)
  1420. elif field.type == 'time':
  1421. value_input = SQLFORM.widgets.time.widget(field,field.default,_id=_id)
  1422. elif field.type == 'date':
  1423. value_input = SQLFORM.widgets.date.widget(field,field.default,_id=_id)
  1424. elif field.type == 'datetime':
  1425. value_input = SQLFORM.widgets.datetime.widget(field,field.default,_id=_id)
  1426. elif (field.type.startswith('reference ') or
  1427. field.type.startswith('list:reference ')) and \
  1428. hasattr(field.requires,'options'):
  1429. value_input = SELECT(
  1430. *[OPTION(v, _value=k)
  1431. for k,v in field.requires.options()],
  1432. **dict(_id=_id))
  1433. elif field.type == 'integer' or \
  1434. field.type.startswith('reference ') or \
  1435. field.type.startswith('list:integer') or \
  1436. field.type.startswith('list:reference '):
  1437. value_input = SQLFORM.widgets.integer.widget(field,field.default,_id=_id)
  1438. else:
  1439. value_input = INPUT(
  1440. _type='text', _id=_id, _class=field.type)
  1441. new_button = INPUT(
  1442. _type="button", _value=T('New'), _class="btn",
  1443. _onclick="%s_build_query('new','%s')" % (prefix,field))
  1444. and_button = INPUT(
  1445. _type="button", _value=T('And'), _class="btn",
  1446. _onclick="%s_build_query('and','%s')" % (prefix, field))
  1447. or_button = INPUT(
  1448. _type="button", _value=T('Or'), _class="btn",
  1449. _onclick="%s_build_query('or','%s')" % (prefix, field))
  1450. close_button = INPUT(
  1451. _type="button", _value=T('Close'), _class="btn",
  1452. _onclick="jQuery('#%s').slideUp()" % panel_id)
  1453. criteria.append(DIV(
  1454. operators, value_input, new_button,
  1455. and_button, or_button, close_button,
  1456. _id='%s_%s' % (field_id, name),
  1457. _class='w2p_query_row hidden',
  1458. _style='display:inline'))
  1459. criteria.insert(0, SELECT(
  1460. _id=fields_id,
  1461. _onchange="jQuery('.w2p_query_row').hide();jQuery('#%s_'+jQuery('#%s').val().replace('.','-')).show();" % (field_id,fields_id),
  1462. _style='float:left',
  1463. *selectfields))
  1464. fadd = SCRIPT("""
  1465. jQuery('#%(fields_id)s input,#%(fields_id)s select').css(
  1466. 'width','auto');
  1467. jQuery(function(){web2py_ajax_fields('#%(fields_id)s');});
  1468. function %(prefix)s_build_query(aggregator,a) {
  1469. var b=a.replace('.','-');
  1470. var option = jQuery('#%(field_id)s_'+b+' select').val();
  1471. var value = jQuery('#%(value_id)s_'+b).val().replace('"','\\\\"');
  1472. var s=a+' '+option+' "'+value+'"';
  1473. var k=jQuery('#%(keywords_id)s');
  1474. var v=k.val();
  1475. if(aggregator=='new') k.val(s); else k.val((v?(v+' '+ aggregator +' '):'')+s);
  1476. }
  1477. """ % dict(
  1478. prefix=prefix,fields_id=fields_id,keywords_id=keywords_id,
  1479. field_id=field_id,value_id=value_id
  1480. )
  1481. )
  1482. return CAT(
  1483. DIV(_id=panel_id, _style="display:none;", *criteria), fadd)
  1484. @staticmethod
  1485. def grid(query,
  1486. fields=None,
  1487. field_id=None,
  1488. left=None,
  1489. headers={},
  1490. orderby=None,
  1491. groupby=None,
  1492. searchable=True,
  1493. sortable=True,
  1494. paginate=20,
  1495. deletable=True,
  1496. editable=True,
  1497. details=True,
  1498. selectable=None,
  1499. create=True,
  1500. csv=True,
  1501. links=None,
  1502. links_in_grid=True,
  1503. upload='<default>',
  1504. args=[],
  1505. user_signature=True,
  1506. maxtextlengths={},
  1507. maxtextlength=20,
  1508. onvalidation=None,
  1509. onfailure=None,
  1510. oncreate=None,
  1511. onupdate=None,
  1512. ondelete=None,
  1513. sorter_icons=(XML('&#x2191;'), XML('&#x2193;')),
  1514. ui = 'web2py',
  1515. showbuttontext=True,
  1516. _class="web2py_grid",
  1517. formname='web2py_grid',
  1518. search_widget='default',
  1519. ignore_rw = False,
  1520. formstyle = 'table3cols',
  1521. exportclasses = None,
  1522. formargs={},
  1523. createargs={},
  1524. editargs={},
  1525. viewargs={},
  1526. selectable_submit_button='Submit',
  1527. buttons_placement = 'right',
  1528. links_placement = 'right',
  1529. noconfirm=False,
  1530. cache_count=None,
  1531. client_side_delete=False,
  1532. ):
  1533. # jQuery UI ThemeRoller classes (empty if ui is disabled)
  1534. if ui == 'jquery-ui':
  1535. ui = dict(widget='ui-widget',
  1536. header='ui-widget-header',
  1537. content='ui-widget-content',
  1538. default='ui-state-default',
  1539. cornerall='ui-corner-all',
  1540. cornertop='ui-corner-top',
  1541. cornerbottom='ui-corner-bottom',
  1542. button='ui-button-text-icon-primary',
  1543. buttontext='ui-button-text',
  1544. buttonadd='ui-icon ui-icon-plusthick',
  1545. buttonback='ui-icon ui-icon-arrowreturnthick-1-w',
  1546. buttonexport='ui-icon ui-icon-transferthick-e-w',
  1547. buttondelete='ui-icon ui-icon-trash',
  1548. buttonedit='ui-icon ui-icon-pencil',
  1549. buttontable='ui-icon ui-icon-triangle-1-e',
  1550. buttonview='ui-icon ui-icon-zoomin',
  1551. )
  1552. elif ui == 'web2py':
  1553. ui = dict(widget='',
  1554. header='',
  1555. content='',
  1556. default='',
  1557. cornerall='',
  1558. cornertop='',
  1559. cornerbottom='',
  1560. button='button btn',
  1561. buttontext='buttontext button',
  1562. buttonadd='icon plus icon-plus',
  1563. buttonback='icon leftarrow icon-arrow-left',
  1564. buttonexport='icon downarrow icon-download',
  1565. buttondelete='icon trash icon-trash',
  1566. buttonedit='icon pen icon-pencil',
  1567. buttontable='icon rightarrow icon-arrow-right',
  1568. buttonview='icon magnifier icon-zoom-in',
  1569. )
  1570. elif not isinstance(ui, dict):
  1571. raise RuntimeError('SQLFORM.grid ui argument must be a dictionary')
  1572. db = query._db
  1573. T = current.T
  1574. request = current.request
  1575. session = current.session
  1576. response = current.response
  1577. logged = session.auth and session.auth.user
  1578. wenabled = (not user_signature or logged)
  1579. create = wenabled and create
  1580. editable = wenabled and editable
  1581. deletable = wenabled and deletable
  1582. rows = None
  1583. def fetch_count(dbset):
  1584. ##FIXME for google:datastore cache_count is ignored
  1585. ## if it's not an integer
  1586. if cache_count is None or isinstance(cache_count, tuple):
  1587. if groupby:
  1588. c = 'count(*)'
  1589. nrows = db.executesql(
  1590. 'select count(*) from (%s);' %
  1591. dbset._select(c, left=left, cacheable=True,
  1592. groupby=groupby, cache=cache_count)[:-1])[0][0]
  1593. elif left:
  1594. c = 'count(*)'
  1595. nrows = dbset.select(c, left=left, cacheable=True, cache=cache_count).first()[c]
  1596. elif dbset._db._adapter.dbengine=='google:datastore':
  1597. #if we don't set a limit, this can timeout for a large table
  1598. nrows = dbset.db._adapter.count(dbset.query, limit=1000)
  1599. else:
  1600. nrows = dbset.count(cache=cache_count)
  1601. elif isinstance(cache_count, (int, long)):
  1602. nrows = cache_count
  1603. elif callable(cache_count):
  1604. nrows = cache_count(dbset, request.vars)
  1605. else:
  1606. nrows = 0
  1607. return nrows
  1608. def url(**b):
  1609. b['args'] = args + b.get('args', [])
  1610. localvars = request.get_vars.copy()
  1611. localvars.update(b.get('vars', {}))
  1612. b['vars'] = localvars
  1613. b['hash_vars'] = False
  1614. b['user_signature'] = user_signature
  1615. return URL(**b)
  1616. def url2(**b):
  1617. b['args'] = request.args + b.get('args', [])
  1618. localvars = request.get_vars.copy()
  1619. localvars.update(b.get('vars', {}))
  1620. b['vars'] = localvars
  1621. b['hash_vars'] = False
  1622. b['user_signature'] = user_signature
  1623. return URL(**b)
  1624. referrer = session.get('_web2py_grid_referrer_' + formname, url())
  1625. # if not user_signature every action is accessible
  1626. # else forbid access unless
  1627. # - url is based url
  1628. # - url has valid signature (vars are not signed, only path_info)
  1629. # = url does not contain 'create','delete','edit' (readonly)
  1630. if user_signature:
  1631. if not (
  1632. '/'.join(str(a) for a in args) == '/'.join(request.args) or
  1633. URL.verify(request,user_signature=user_signature,
  1634. hash_vars=False) or
  1635. (request.args(len(args))=='view' and not logged)):
  1636. session.flash = T('not authorized')
  1637. redirect(referrer)
  1638. def gridbutton(buttonclass='buttonadd', buttontext=T('Add'),
  1639. buttonurl=url(args=[]), callback=None,
  1640. delete=None, trap=True, noconfirm=None):
  1641. if showbuttontext:
  1642. return A(SPAN(_class=ui.get(buttonclass)),
  1643. SPAN(T(buttontext), _title=T(buttontext),
  1644. _class=ui.get('buttontext')),
  1645. _href=buttonurl,
  1646. callback=callback,
  1647. delete=delete,
  1648. noconfirm=noconfirm,
  1649. _class=trap_class(ui.get('button'), trap))
  1650. else:
  1651. return A(SPAN(_class=ui.get(buttonclass)),
  1652. _href=buttonurl,
  1653. callback=callback,
  1654. delete=delete,
  1655. noconfirm=noconfirm,
  1656. _title=T(buttontext),
  1657. _class=trap_class(ui.get('buttontext'), trap))
  1658. dbset = db(query)
  1659. tablenames = db._adapter.tables(dbset.query)
  1660. if left is not None:
  1661. if not isinstance(left, (list, tuple)):
  1662. left = [left]
  1663. for join in left:
  1664. tablenames += db._adapter.tables(join)
  1665. tables = [db[tablename] for tablename in tablenames]
  1666. if fields:
  1667. columns = [f for f in fields if f.tablename in tablenames]
  1668. else:
  1669. fields = []
  1670. columns = []
  1671. for table in tables:
  1672. fields += [f for f in table]
  1673. columns += [f for f in table]
  1674. for k,f in table.iteritems():
  1675. if isinstance(f,Field.Virtual) and f.readable:
  1676. f.tablename = table._tablename
  1677. columns.append(f)
  1678. fields.append(f)
  1679. if not field_id:
  1680. field_id = tables[0]._id
  1681. if not any(str(f)==str(field_id) for f in fields):
  1682. fields = [f for f in fields]+[field_id]
  1683. table = field_id.table
  1684. tablename = table._tablename
  1685. if upload == '<default>':
  1686. upload = lambda filename: url(args=['download', filename])
  1687. if request.args(-2) == 'download':
  1688. stream = response.download(request, db)
  1689. raise HTTP(200, stream, **response.headers)
  1690. def buttons(edit=False, view=False, record=None):
  1691. buttons = DIV(gridbutton('buttonback', 'Back', referrer),
  1692. _class='form_header row_buttons %(header)s %(cornertop)s' % ui)
  1693. if edit and (not callable(edit) or edit(record)):
  1694. args = ['edit', table._tablename, request.args[-1]]
  1695. buttons.append(gridbutton('buttonedit', 'Edit',
  1696. url(args=args)))
  1697. if view:
  1698. args = ['view', table._tablename, request.args[-1]]
  1699. buttons.append(gridbutton('buttonview', 'View',
  1700. url(args=args)))
  1701. if record and links:
  1702. for link in links:
  1703. if isinstance(link, dict):
  1704. buttons.append(link['body'](record))
  1705. elif link(record):
  1706. buttons.append(link(record))
  1707. return buttons
  1708. def linsert(lst, i, x):
  1709. """
  1710. a = [1,2]
  1711. linsert(a, 1, [0,3])
  1712. a = [1, 0, 3, 2]
  1713. """
  1714. lst[i:i] = x
  1715. formfooter = DIV(
  1716. _class='form_footer row_buttons %(header)s %(cornerbottom)s' % ui)
  1717. create_form = update_form = view_form = search_form = None
  1718. sqlformargs = dict(formargs)
  1719. if create and request.args(-2) == 'new':
  1720. table = db[request.args[-1]]
  1721. sqlformargs.update(createargs)
  1722. create_form = SQLFORM(
  1723. table, ignore_rw=ignore_rw, formstyle=formstyle,
  1724. _class='web2py_form',
  1725. **sqlformargs)
  1726. create_form.process(formname=formname,
  1727. next=referrer,
  1728. onvalidation=onvalidation,
  1729. onfailure=onfailure,
  1730. onsuccess=oncreate)
  1731. res = DIV(buttons(), create_form, formfooter, _class=_class)
  1732. res.create_form = create_form
  1733. res.update_form = update_form
  1734. res.view_form = view_form
  1735. res.search_form = search_form
  1736. res.rows = None
  1737. return res
  1738. elif details and request.args(-3) == 'view':
  1739. table = db[request.args[-2]]
  1740. record = table(request.args[-1]) or redirect(referrer)
  1741. sqlformargs.update(viewargs)
  1742. view_form = SQLFORM(
  1743. table, record, upload=upload, ignore_rw=ignore_rw,
  1744. formstyle=formstyle, readonly=True, _class='web2py_form',
  1745. **sqlformargs)
  1746. res = DIV(buttons(edit=editable, record=record), view_form,
  1747. formfooter, _class=_class)
  1748. res.create_form = create_form
  1749. res.update_form = update_form
  1750. res.view_form = view_form
  1751. res.search_form = search_form
  1752. res.rows = None
  1753. return res
  1754. elif editable and request.args(-3) == 'edit':
  1755. table = db[request.args[-2]]
  1756. record = table(request.args[-1]) or redirect(URL('error'))
  1757. sqlformargs.update(editargs)
  1758. deletable_ = deletable(record) if callable(deletable) else deletable
  1759. update_form = SQLFORM(
  1760. table,
  1761. record, upload=upload, ignore_rw=ignore_rw,
  1762. formstyle=formstyle, deletable=deletable_,
  1763. _class='web2py_form',
  1764. submit_button=T('Submit'),
  1765. delete_label=T('Check to delete'),
  1766. **sqlformargs)
  1767. update_form.process(
  1768. formname=formname,
  1769. onvalidation=onvalidation,
  1770. onfailure=onfailure,
  1771. onsuccess=onupdate,
  1772. next=referrer)
  1773. res = DIV(buttons(view=details, record=record),
  1774. update_form, formfooter, _class=_class)
  1775. res.create_form = create_form
  1776. res.update_form = update_form
  1777. res.view_form = view_form
  1778. res.search_form = search_form
  1779. res.rows = None
  1780. return res
  1781. elif deletable and request.args(-3) == 'delete':
  1782. table = db[request.args[-2]]
  1783. if not callable(deletable):
  1784. if ondelete:
  1785. ondelete(table, request.args[-1])
  1786. db(table[table._id.name] == request.args[-1]).delete()
  1787. else:
  1788. record = table(request.args[-1]) or redirect(URL('error'))
  1789. if deletable(record):
  1790. if ondelete:
  1791. ondelete(table, request.args[-1])
  1792. record.delete_record()
  1793. redirect(referrer, client_side=client_side_delete)
  1794. exportManager = dict(
  1795. csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'),
  1796. csv=(ExporterCSV, 'CSV'),
  1797. xml=(ExporterXML, 'XML'),
  1798. html=(ExporterHTML, 'HTML'),
  1799. json=(ExporterJSON, 'JSON'),
  1800. tsv_with_hidden_cols=
  1801. (ExporterTSV, 'TSV (Excel compatible, hidden cols)'),
  1802. tsv=(ExporterTSV, 'TSV (Excel compatible)'))
  1803. if not exportclasses is None:
  1804. """
  1805. remember: allow to set exportclasses=dict(csv=False) to disable the csv format
  1806. """
  1807. exportManager.update(exportclasses)
  1808. export_type = request.vars._export_type
  1809. if export_type:
  1810. order = request.vars.order or ''
  1811. if sortable:
  1812. if order and not order == 'None':
  1813. if order[:1] == '~':
  1814. sign, rorder = '~', order[1:]
  1815. else:
  1816. sign, rorder = '', order
  1817. tablename, fieldname = rorder.split('.', 1)
  1818. orderby = db[tablename][fieldname]
  1819. if sign == '~':
  1820. orderby = ~orderby
  1821. expcolumns = [str(f) for f in columns]
  1822. if export_type.endswith('with_hidden_cols'):
  1823. expcolumns = []
  1824. for table in tables:
  1825. for field in table:
  1826. if field.readable and field.tablename in tablenames:
  1827. expcolumns.append(field)
  1828. if export_type in exportManager and exportManager[export_type]:
  1829. if request.vars.keywords:
  1830. try:
  1831. dbset = dbset(SQLFORM.build_query(
  1832. fields, request.vars.get('keywords', '')))
  1833. rows = dbset.select(cacheable=True, *expcolumns)
  1834. except Exception, e:
  1835. response.flash = T('Internal Error')
  1836. rows = []
  1837. else:
  1838. rows = dbset.select(left=left, orderby=orderby,
  1839. cacheable=True, *expcolumns)
  1840. value = exportManager[export_type]
  1841. clazz = value[0] if hasattr(value, '__getitem__') else value
  1842. oExp = clazz(rows)
  1843. filename = '.'.join(('rows', oExp.file_ext))
  1844. response.headers['Content-Type'] = oExp.content_type
  1845. response.headers['Content-Disposition'] = \
  1846. 'attachment;filename=' + filename + ';'
  1847. raise HTTP(200, oExp.export(), **response.headers)
  1848. elif request.vars.records and not isinstance(
  1849. request.vars.records, list):
  1850. request.vars.records = [request.vars.records]
  1851. elif not request.vars.records:
  1852. request.vars.records = []
  1853. session['_web2py_grid_referrer_' + formname] = \
  1854. url2(vars=request.get_vars)
  1855. console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui)
  1856. error = None
  1857. if create:
  1858. add = gridbutton(
  1859. buttonclass='buttonadd',
  1860. buttontext=T('Add'),
  1861. buttonurl=url(args=['new', tablename]))
  1862. if not searchable:
  1863. console.append(add)
  1864. else:
  1865. add = ''
  1866. if searchable:
  1867. sfields = reduce(lambda a, b: a + b,
  1868. [[f for f in t if f.readable] for t in tables])
  1869. if isinstance(search_widget, dict):
  1870. search_widget = search_widget[tablename]
  1871. if search_widget == 'default':
  1872. prefix = formname == 'web2py_grid' and 'w2p' or 'w2p_%s' % formname
  1873. search_menu = SQLFORM.search_menu(sfields, prefix=prefix)
  1874. spanel_id = '%s_query_fields' % prefix
  1875. sfields_id = '%s_query_panel' % prefix
  1876. skeywords_id = '%s_keywords' % prefix
  1877. search_widget = lambda sfield, url: CAT(FORM(
  1878. INPUT(_name='keywords', _value=request.vars.keywords,
  1879. _id=skeywords_id,
  1880. _onfocus="jQuery('#%s').change();jQuery('#%s').slideDown();" % (spanel_id, sfields_id)),
  1881. INPUT(_type='submit', _value=T('Search'), _class="btn"),
  1882. INPUT(_type='submit', _value=T('Clear'), _class="btn",
  1883. _onclick="jQuery('#%s').val('');" % skeywords_id),
  1884. _method="GET", _action=url), search_menu)
  1885. form = search_widget and search_widget(sfields, url()) or ''
  1886. console.append(add)
  1887. console.append(form)
  1888. keywords = request.vars.get('keywords', '')
  1889. try:
  1890. if callable(searchable):
  1891. subquery = searchable(sfields, keywords)
  1892. else:
  1893. subquery = SQLFORM.build_query(sfields, keywords)
  1894. except RuntimeError:
  1895. subquery = None
  1896. error = T('Invalid query')
  1897. else:
  1898. subquery = None
  1899. if subquery:
  1900. dbset = dbset(subquery)
  1901. try:
  1902. nrows = fetch_count(dbset)
  1903. except:
  1904. nrows = 0
  1905. error = T('Unsupported query')
  1906. order = request.vars.order or ''
  1907. if sortable:
  1908. if order and not order == 'None':
  1909. tablename, fieldname = order.split('~')[-1].split('.', 1)
  1910. sort_field = db[tablename][fieldname]
  1911. exception = sort_field.type in ('date', 'datetime', 'time')
  1912. if exception:
  1913. orderby = (order[:1] == '~' and sort_field) or ~sort_field
  1914. else:
  1915. orderby = (order[:1] == '~' and ~sort_field) or sort_field
  1916. headcols = []
  1917. if selectable:
  1918. headcols.append(TH(_class=ui.get('default')))
  1919. for field in columns:
  1920. if not field.readable:
  1921. continue
  1922. key = str(field)
  1923. header = headers.get(str(field), field.label or key)
  1924. if sortable and not isinstance(field, Field.Virtual):
  1925. if key == order:
  1926. key, marker = '~' + order, sorter_icons[0]
  1927. elif key == order[1:]:
  1928. marker = sorter_icons[1]
  1929. else:
  1930. marker = ''
  1931. header = A(header, marker, _href=url(vars=dict(
  1932. keywords=request.vars.keywords or '',
  1933. order=key)), _class=trap_class())
  1934. headcols.append(TH(header, _class=ui.get('default')))
  1935. toadd = []
  1936. if links and links_in_grid:
  1937. for link in links:
  1938. if isinstance(link, dict):
  1939. toadd.append(TH(link['header'], _class=ui.get('default')))
  1940. if links_placement in ['right', 'both']:
  1941. headcols.extend(toadd)
  1942. if links_placement in ['left', 'both']:
  1943. linsert(headcols, 0, toadd)
  1944. # Include extra column for buttons if needed.
  1945. include_buttons_column = (details or editable or deletable or
  1946. (links and links_in_grid and
  1947. not all([isinstance(link, dict) for link in links])))
  1948. if include_buttons_column:
  1949. if buttons_placement in ['right', 'both']:
  1950. headcols.append(TH(_class=ui.get('default','')))
  1951. if buttons_placement in ['left', 'both']:
  1952. headcols.insert(0, TH(_class=ui.get('default','')))
  1953. head = TR(*headcols, **dict(_class=ui.get('header')))
  1954. cursor = True
  1955. #figure out what page we are one to setup the limitby
  1956. if paginate and dbset._db._adapter.dbengine=='google:datastore':
  1957. cursor = request.vars.cursor or True
  1958. limitby = (0, paginate)
  1959. try: page = int(request.vars.page or 1)-1
  1960. except ValueError: page = 0
  1961. elif paginate and paginate<nrows:
  1962. try: page = int(request.vars.page or 1)-1
  1963. except ValueError: page = 0
  1964. limitby = (paginate*page,paginate*(page+1))
  1965. else:
  1966. limitby = None
  1967. try:
  1968. table_fields = filter(lambda f: f.tablename in tablenames, fields)
  1969. if dbset._db._adapter.dbengine=='google:datastore':
  1970. rows = dbset.select(left=left,orderby=orderby,
  1971. groupby=groupby,limitby=limitby,
  1972. reusecursor=cursor,
  1973. cacheable=True,*table_fields)
  1974. next_cursor = dbset._db.get('_lastcursor', None)
  1975. else:
  1976. # print('table_fields: %s' %([f_.name for f_ in table_fields],))
  1977. rows = dbset.select(left=left,orderby=orderby,
  1978. groupby=groupby,limitby=limitby,
  1979. cacheable=True,*table_fields)
  1980. except SyntaxError:
  1981. rows = None
  1982. next_cursor = None
  1983. error = T("Query Not Supported")
  1984. except Exception, e:
  1985. rows = None
  1986. next_cursor = None
  1987. error = T("Query Not Supported: %s")%e
  1988. message = error
  1989. if not message and nrows:
  1990. if dbset._db._adapter.dbengine=='google:datastore' and nrows>=1000:
  1991. message = T('at least %(nrows)s records found') % dict(nrows=nrows)
  1992. else:
  1993. message = T('%(nrows)s records found') % dict(nrows=nrows)
  1994. console.append(DIV(message or T('None'),_class='web2py_counter'))
  1995. paginator = UL()
  1996. if paginate and dbset._db._adapter.dbengine=='google:datastore':
  1997. #this means we may have a large table with an unknown number of rows.
  1998. try:
  1999. page = int(request.vars.page or 1)-1
  2000. except ValueError:
  2001. page = 0
  2002. paginator.append(LI('page %s'%(page+1)))
  2003. if next_cursor:
  2004. d = dict(page=page+2, cursor=next_cursor)
  2005. if order: d['order']=order
  2006. if request.vars.keywords: d['keywords']=request.vars.keywords
  2007. paginator.append(LI(
  2008. A('next',_href=url(vars=d),_class=trap_class())))
  2009. elif paginate and paginate<nrows:
  2010. npages, reminder = divmod(nrows, paginate)
  2011. if reminder:
  2012. npages += 1
  2013. try:
  2014. page = int(request.vars.page or 1) - 1
  2015. except ValueError:
  2016. page = 0
  2017. def self_link(name, p):
  2018. d = dict(page=p + 1)
  2019. if order:
  2020. d['order'] = order
  2021. if request.vars.keywords:
  2022. d['keywords'] = request.vars.keywords
  2023. return A(name, _href=url(vars=d), _class=trap_class())
  2024. NPAGES = 5 # window is 2*NPAGES
  2025. if page > NPAGES + 1:
  2026. paginator.append(LI(self_link('<<', 0)))
  2027. if page > NPAGES:
  2028. paginator.append(LI(self_link('<', page - 1)))
  2029. pages = range(max(0, page - NPAGES), min(page + NPAGES, npages))
  2030. for p in pages:
  2031. if p == page:
  2032. paginator.append(LI(A(p + 1, _onclick='return false'),
  2033. _class=trap_class('current')))
  2034. else:
  2035. paginator.append(LI(self_link(p + 1, p)))
  2036. if page < npages - NPAGES:
  2037. paginator.append(LI(self_link('>', page + 1)))
  2038. if page < npages - NPAGES - 1:
  2039. paginator.append(LI(self_link('>>', npages - 1)))
  2040. else:
  2041. limitby = None
  2042. if rows:
  2043. htmltable = TABLE(THEAD(head))
  2044. tbody = TBODY()
  2045. numrec = 0
  2046. for row in rows:
  2047. trcols = []
  2048. id = row[field_id]
  2049. if selectable:
  2050. trcols.append(
  2051. INPUT(_type="checkbox", _name="records", _value=id,
  2052. value=request.vars.records))
  2053. for field in columns:
  2054. if not field.readable:
  2055. continue
  2056. if field.type == 'blob':
  2057. continue
  2058. value = row[str(field)]
  2059. maxlength = maxtextlengths.get(str(field), maxtextlength)
  2060. if field.represent:
  2061. try:
  2062. value = field.represent(value, row)
  2063. except KeyError:
  2064. try:
  2065. value = field.represent(
  2066. value, row[field.tablename])
  2067. except KeyError:
  2068. pass
  2069. elif field.type == 'boolean':
  2070. value = INPUT(_type="checkbox", _checked=value,
  2071. _disabled=True)
  2072. elif field.type == 'upload':
  2073. if value:
  2074. if callable(upload):
  2075. value = A(
  2076. T('file'), _href=upload(value))
  2077. elif upload:
  2078. value = A(T('file'),
  2079. _href='%s/%s' % (upload, value))
  2080. else:
  2081. value = ''
  2082. if isinstance(value, str):
  2083. value = truncate_string(value, maxlength)
  2084. elif not isinstance(value, DIV):
  2085. value = field.formatter(value)
  2086. trcols.append(TD(value))
  2087. row_buttons = TD(_class='row_buttons',_nowrap=True)
  2088. if links and links_in_grid:
  2089. toadd = []
  2090. for link in links:
  2091. if isinstance(link, dict):
  2092. toadd.append(TD(link['body'](row)))
  2093. else:
  2094. if link(row):
  2095. row_buttons.append(link(row))
  2096. if links_placement in ['right', 'both']:
  2097. trcols.extend(toadd)
  2098. if links_placement in ['left', 'both']:
  2099. linsert(trcols, 0, toadd)
  2100. if include_buttons_column:
  2101. if details and (not callable(details) or details(row)):
  2102. row_buttons.append(gridbutton(
  2103. 'buttonview', 'View',
  2104. url(args=['view', tablename, id])))
  2105. if editable and (not callable(editable) or editable(row)):
  2106. row_buttons.append(gridbutton(
  2107. 'buttonedit', 'Edit',
  2108. url(args=['edit', tablename, id])))
  2109. if deletable and (not callable(deletable) or deletable(row)):
  2110. row_buttons.append(gridbutton(
  2111. 'buttondelete', 'Delete',
  2112. url(args=['delete', tablename, id]),
  2113. callback=url(args=['delete', tablename, id]),
  2114. noconfirm=noconfirm,
  2115. delete='tr'))
  2116. if buttons_placement in ['right', 'both']:
  2117. trcols.append(row_buttons)
  2118. if buttons_placement in ['left', 'both']:
  2119. trcols.insert(0, row_buttons)
  2120. if numrec % 2 == 0:
  2121. classtr = 'even'
  2122. else:
  2123. classtr = 'odd'
  2124. numrec += 1
  2125. if id:
  2126. rid = id
  2127. if callable(rid): # can this ever be callable?
  2128. rid = rid(row)
  2129. tr = TR(*trcols, **dict(
  2130. _id=rid,
  2131. _class='%s %s' % (classtr, 'with_id')))
  2132. else:
  2133. tr = TR(*trcols, **dict(_class=classtr))
  2134. tbody.append(tr)
  2135. htmltable.append(tbody)
  2136. htmltable = DIV(
  2137. htmltable, _class='web2py_htmltable',
  2138. _style='width:100%;overflow-x:auto;-ms-overflow-x:scroll')
  2139. if selectable:
  2140. if not callable(selectable):
  2141. #now expect that selectable and related parameters are iterator (list, tuple, etc)
  2142. inputs = []
  2143. for i, submit_info in enumerate(selectable):
  2144. submit_text = submit_info[0]
  2145. submit_class = submit_info[2] if len(submit_info) > 2 else ''
  2146. input_ctrl = INPUT(_type="submit", _name='submit_%d' % i, _value=T(submit_text))
  2147. input_ctrl.add_class(submit_class)
  2148. inputs.append(input_ctrl)
  2149. else:
  2150. inputs = [INPUT(_type="submit", _value=T(selectable_submit_button))]
  2151. if formstyle == 'bootstrap':
  2152. # add space between buttons
  2153. #inputs = sum([[inp, ' '] for inp in inputs], [])[:-1]
  2154. htmltable = FORM(htmltable, DIV(_class='form-actions', *inputs))
  2155. else:
  2156. htmltable = FORM(htmltable, *inputs)
  2157. if htmltable.process(formname=formname).accepted:
  2158. htmltable.vars.records = htmltable.vars.records or []
  2159. htmltable.vars.records = htmltable.vars.records if type(htmltable.vars.records) == list else [htmltable.vars.records]
  2160. records = [int(r) for r in htmltable.vars.records]
  2161. if not callable(selectable):
  2162. for i, submit_info in enumerate(selectable):
  2163. submit_callback = submit_info[1]
  2164. if htmltable.vars.get('submit_%d' % i, False):
  2165. submit_callback(records)
  2166. break
  2167. else:
  2168. selectable(records)
  2169. redirect(referrer)
  2170. else:
  2171. htmltable = DIV(T('No records found'))
  2172. if csv and nrows:
  2173. export_links = []
  2174. for k, v in sorted(exportManager.items()):
  2175. if not v:
  2176. continue
  2177. label = v[1] if hasattr(v, "__getitem__") else k
  2178. link = url2(vars=dict(
  2179. order=request.vars.order or '',
  2180. _export_type=k,
  2181. keywords=request.vars.keywords or ''))
  2182. export_links.append(A(T(label), _href=link))
  2183. export_menu = \
  2184. DIV(T('Export:'), _class="w2p_export_menu", *export_links)
  2185. else:
  2186. export_menu = None
  2187. res = DIV(console, DIV(htmltable, _class="web2py_table"),
  2188. _class='%s %s' % (_class, ui.get('widget')))
  2189. if paginator.components:
  2190. res.append(
  2191. DIV(paginator,
  2192. _class="web2py_paginator %(header)s %(cornerbottom)s" % ui))
  2193. if export_menu:
  2194. res.append(export_menu)
  2195. res.create_form = create_form
  2196. res.update_form = update_form
  2197. res.view_form = view_form
  2198. res.search_form = search_form
  2199. res.rows = rows
  2200. return res
  2201. @staticmethod
  2202. def smartgrid(table, constraints=None, linked_tables=None,
  2203. links=None, links_in_grid=True,
  2204. args=None, user_signature=True,
  2205. divider='>', breadcrumbs_class='',
  2206. **kwargs):
  2207. """
  2208. @auth.requires_login()
  2209. def index():
  2210. db.define_table('person',Field('name'),format='%(name)s')
  2211. db.define_table('dog',
  2212. Field('name'),Field('owner',db.person),format='%(name)s')
  2213. db.define_table('comment',Field('body'),Field('dog',db.dog))
  2214. if db(db.person).isempty():
  2215. from gluon.contrib.populate import populate
  2216. populate(db.person,300)
  2217. populate(db.dog,300)
  2218. populate(db.comment,1000)
  2219. db.commit()
  2220. form=SQLFORM.smartgrid(db[request.args(0) or 'person']) #***
  2221. return dict(form=form)
  2222. *** builds a complete interface to navigate all tables links
  2223. to the request.args(0)
  2224. table: pagination, search, view, edit, delete,
  2225. children, parent, etc.
  2226. constraints is a dict {'table':query} that limits which
  2227. records can be accessible
  2228. links is a dict like
  2229. {'tablename':[lambda row: A(....), ...]}
  2230. that will add buttons when table tablename is displayed
  2231. linked_tables is a optional list of tablenames of tables
  2232. to be linked
  2233. """
  2234. request, T = current.request, current.T
  2235. if args is None:
  2236. args = []
  2237. def url(**b):
  2238. b['args'] = request.args[:nargs] + b.get('args', [])
  2239. b['hash_vars'] = False
  2240. b['user_signature'] = user_signature
  2241. return URL(**b)
  2242. db = table._db
  2243. breadcrumbs = []
  2244. if request.args(len(args)) != table._tablename:
  2245. request.args[:] = args + [table._tablename]
  2246. if links is None:
  2247. links = {}
  2248. if constraints is None:
  2249. constraints = {}
  2250. field = None
  2251. name = None
  2252. def format(table,row):
  2253. if not row:
  2254. return T('Unknown')
  2255. elif isinstance(table._format,str):
  2256. return table._format % row
  2257. elif callable(table._format):
  2258. return table._format(row)
  2259. else:
  2260. return '#'+str(row.id)
  2261. try:
  2262. nargs = len(args) + 1
  2263. previous_tablename, previous_fieldname, previous_id = \
  2264. table._tablename, None, None
  2265. while len(request.args) > nargs:
  2266. key = request.args(nargs)
  2267. if '.' in key:
  2268. id = request.args(nargs + 1)
  2269. tablename, fieldname = key.split('.', 1)
  2270. table = db[tablename]
  2271. field = table[fieldname]
  2272. field.default = id
  2273. referee = field.type[10:]
  2274. if referee != previous_tablename:
  2275. raise HTTP(400)
  2276. cond = constraints.get(referee, None)
  2277. if cond:
  2278. record = db(
  2279. db[referee]._id == id)(cond).select().first()
  2280. else:
  2281. record = db[referee](id)
  2282. if previous_id:
  2283. if record[previous_fieldname] != int(previous_id):
  2284. raise HTTP(400)
  2285. previous_tablename, previous_fieldname, previous_id = \
  2286. tablename, fieldname, id
  2287. name = format(db[referee],record)
  2288. breadcrumbs.append(
  2289. LI(A(T(db[referee]._plural),
  2290. _class=trap_class(),
  2291. _href=url()),
  2292. SPAN(divider, _class='divider'),
  2293. _class='w2p_grid_breadcrumb_elem'))
  2294. if kwargs.get('details', True):
  2295. breadcrumbs.append(
  2296. LI(A(name, _class=trap_class(),
  2297. _href=url(args=['view', referee, id])),
  2298. SPAN(divider, _class='divider'),
  2299. _class='w2p_grid_breadcrumb_elem'))
  2300. nargs += 2
  2301. else:
  2302. break
  2303. if nargs > len(args) + 1:
  2304. query = (field == id)
  2305. # cjk
  2306. # if isinstance(linked_tables, dict):
  2307. # linked_tables = linked_tables.get(table._tablename, [])
  2308. if linked_tables is None or referee in linked_tables:
  2309. 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]))
  2310. except (KeyError, ValueError, TypeError):
  2311. redirect(URL(args=table._tablename))
  2312. if nargs == len(args) + 1:
  2313. query = table._db._adapter.id_query(table)
  2314. # filter out data info for displayed table
  2315. if table._tablename in constraints:
  2316. query = query & constraints[table._tablename]
  2317. if isinstance(links, dict):
  2318. links = links.get(table._tablename, [])
  2319. for key in 'columns,orderby,searchable,sortable,paginate,deletable,editable,details,selectable,create,fields'.split(','):
  2320. if isinstance(kwargs.get(key, None), dict):
  2321. if table._tablename in kwargs[key]:
  2322. kwargs[key] = kwargs[key][table._tablename]
  2323. else:
  2324. del kwargs[key]
  2325. check = {}
  2326. id_field_name = table._id.name
  2327. for rfield in table._referenced_by:
  2328. check[rfield.tablename] = \
  2329. check.get(rfield.tablename, []) + [rfield.name]
  2330. if linked_tables is None:
  2331. linked_tables = db.tables()
  2332. if isinstance(linked_tables, dict):
  2333. linked_tables = linked_tables.get(table._tablename,[])
  2334. if linked_tables:
  2335. for item in linked_tables:
  2336. tb = None
  2337. if isinstance(item,Table) and item._tablename in check:
  2338. tablename = item._tablename
  2339. linked_fieldnames = check[tablename]
  2340. td = item
  2341. elif isinstance(item,str) and item in check:
  2342. tablename = item
  2343. linked_fieldnames = check[item]
  2344. tb = db[item]
  2345. elif isinstance(item,Field) and item.name in check.get(item._tablename,[]):
  2346. tablename = item._tablename
  2347. linked_fieldnames = [item.name]
  2348. tb = item.table
  2349. else:
  2350. linked_fieldnames = []
  2351. if tb:
  2352. multiple_links = len(linked_fieldnames) > 1
  2353. for fieldname in linked_fieldnames:
  2354. t = T(tb._plural) if not multiple_links else \
  2355. T(tb._plural + '(' + fieldname + ')')
  2356. args0 = tablename + '.' + fieldname
  2357. links.append(
  2358. lambda row, t=t, nargs=nargs, args0=args0:
  2359. A(SPAN(t), _class=trap_class(), _href=url(
  2360. args=[args0, row[id_field_name]])))
  2361. grid = SQLFORM.grid(query, args=request.args[:nargs], links=links,
  2362. links_in_grid=links_in_grid,
  2363. user_signature=user_signature, **kwargs)
  2364. if isinstance(grid, DIV):
  2365. header = table._plural
  2366. next = grid.create_form or grid.update_form or grid.view_form
  2367. breadcrumbs.append(LI(
  2368. A(T(header), _class=trap_class(),_href=url()),
  2369. SPAN(divider, _class='divider') if next else '',
  2370. _class='active w2p_grid_breadcrumb_elem'))
  2371. if grid.create_form:
  2372. header = T('New %(entity)s') % dict(entity=table._singular)
  2373. elif grid.update_form:
  2374. header = T('Edit %(entity)s') % dict(
  2375. entity=format(grid.update_form.table,
  2376. grid.update_form.record))
  2377. elif grid.view_form:
  2378. header = T('View %(entity)s') % dict(
  2379. entity=format(grid.view_form.table,
  2380. grid.view_form.record))
  2381. if next:
  2382. breadcrumbs.append(LI(
  2383. A(T(header), _class=trap_class(),_href=url()),
  2384. _class='active w2p_grid_breadcrumb_elem'))
  2385. grid.insert(
  2386. 0, DIV(UL(*breadcrumbs, **{'_class': breadcrumbs_class}),
  2387. _class='web2py_breadcrumbs'))
  2388. return grid
  2389. class SQLTABLE(TABLE):
  2390. """
  2391. given a Rows object, as returned by a db().select(), generates
  2392. an html table with the rows.
  2393. optional arguments:
  2394. :param linkto: URL (or lambda to generate a URL) to edit individual records
  2395. :param upload: URL to download uploaded files
  2396. :param orderby: Add an orderby link to column headers.
  2397. :param headers: dictionary of headers to headers redefinions
  2398. headers can also be a string to gerenare the headers from data
  2399. for now only headers="fieldname:capitalize",
  2400. headers="labels" and headers=None are supported
  2401. :param truncate: length at which to truncate text in table cells.
  2402. Defaults to 16 characters.
  2403. :param columns: a list or dict contaning the names of the columns to be shown
  2404. Defaults to all
  2405. Optional names attributes for passed to the <table> tag
  2406. The keys of headers and columns must be of the form "tablename.fieldname"
  2407. Simple linkto example::
  2408. rows = db.select(db.sometable.ALL)
  2409. table = SQLTABLE(rows, linkto='someurl')
  2410. This will link rows[id] to .../sometable/value_of_id
  2411. More advanced linkto example::
  2412. def mylink(field, type, ref):
  2413. return URL(args=[field])
  2414. rows = db.select(db.sometable.ALL)
  2415. table = SQLTABLE(rows, linkto=mylink)
  2416. This will link rows[id] to
  2417. current_app/current_controlle/current_function/value_of_id
  2418. New Implements: 24 June 2011:
  2419. -----------------------------
  2420. :param selectid: The id you want to select
  2421. :param renderstyle: Boolean render the style with the table
  2422. :param extracolumns = [{'label':A('Extra',_href='#'),
  2423. 'class': '', #class name of the header
  2424. 'width':'', #width in pixels or %
  2425. 'content':lambda row, rc: A('Edit',_href='edit/%s'%row.id),
  2426. 'selected': False #agregate class selected to this column
  2427. }]
  2428. :param headers = {'table.id':{'label':'Id',
  2429. 'class':'', #class name of the header
  2430. 'width':'', #width in pixels or %
  2431. 'truncate': 16, #truncate the content to...
  2432. 'selected': False #agregate class selected to this column
  2433. },
  2434. 'table.myfield':{'label':'My field',
  2435. 'class':'', #class name of the header
  2436. 'width':'', #width in pixels or %
  2437. 'truncate': 16, #truncate the content to...
  2438. 'selected': False #agregate class selected to this column
  2439. },
  2440. }
  2441. table = SQLTABLE(rows, headers=headers, extracolumns=extracolumns)
  2442. `<
  2443. """
  2444. def __init__(
  2445. self,
  2446. sqlrows,
  2447. linkto=None,
  2448. upload=None,
  2449. orderby=None,
  2450. headers={},
  2451. truncate=16,
  2452. columns=None,
  2453. th_link='',
  2454. extracolumns=None,
  2455. selectid=None,
  2456. renderstyle=False,
  2457. cid=None,
  2458. **attributes
  2459. ):
  2460. TABLE.__init__(self, **attributes)
  2461. self.components = []
  2462. self.attributes = attributes
  2463. self.sqlrows = sqlrows
  2464. (components, row) = (self.components, [])
  2465. if not sqlrows:
  2466. return
  2467. if not columns:
  2468. columns = sqlrows.colnames
  2469. if headers == 'fieldname:capitalize':
  2470. headers = {}
  2471. for c in columns:
  2472. headers[c] = c.split('.')[-1].replace('_', ' ').title()
  2473. elif headers == 'labels':
  2474. headers = {}
  2475. for c in columns:
  2476. (t, f) = c.split('.')
  2477. field = sqlrows.db[t][f]
  2478. headers[c] = field.label
  2479. if headers is None:
  2480. headers = {}
  2481. else:
  2482. for c in columns: # new implement dict
  2483. if isinstance(headers.get(c, c), dict):
  2484. coldict = headers.get(c, c)
  2485. attrcol = dict()
  2486. if coldict['width'] != "":
  2487. attrcol.update(_width=coldict['width'])
  2488. if coldict['class'] != "":
  2489. attrcol.update(_class=coldict['class'])
  2490. row.append(TH(coldict['label'], **attrcol))
  2491. elif orderby:
  2492. row.append(TH(A(headers.get(c, c),
  2493. _href=th_link + '?orderby=' + c, cid=cid)))
  2494. else:
  2495. row.append(TH(headers.get(c, c)))
  2496. if extracolumns: # new implement dict
  2497. for c in extracolumns:
  2498. attrcol = dict()
  2499. if c['width'] != "":
  2500. attrcol.update(_width=c['width'])
  2501. if c['class'] != "":
  2502. attrcol.update(_class=c['class'])
  2503. row.append(TH(c['label'], **attrcol))
  2504. components.append(THEAD(TR(*row)))
  2505. tbody = []
  2506. for (rc, record) in enumerate(sqlrows):
  2507. row = []
  2508. if rc % 2 == 0:
  2509. _class = 'even'
  2510. else:
  2511. _class = 'odd'
  2512. if not selectid is None: # new implement
  2513. if record.get('id') == selectid:
  2514. _class += ' rowselected'
  2515. for colname in columns:
  2516. if not table_field.match(colname):
  2517. if "_extra" in record and colname in record._extra:
  2518. r = record._extra[colname]
  2519. row.append(TD(r))
  2520. continue
  2521. else:
  2522. raise KeyError(
  2523. "Column %s not found (SQLTABLE)" % colname)
  2524. (tablename, fieldname) = colname.split('.')
  2525. try:
  2526. field = sqlrows.db[tablename][fieldname]
  2527. except (KeyError, AttributeError):
  2528. field = None
  2529. if tablename in record \
  2530. and isinstance(record, Row) \
  2531. and isinstance(record[tablename], Row):
  2532. r = record[tablename][fieldname]
  2533. elif fieldname in record:
  2534. r = record[fieldname]
  2535. else:
  2536. raise SyntaxError('something wrong in Rows object')
  2537. r_old = r
  2538. if not field or isinstance(field, (Field.Virtual, Field.Lazy)):
  2539. pass
  2540. elif linkto and field.type == 'id':
  2541. try:
  2542. href = linkto(r, 'table', tablename)
  2543. except TypeError:
  2544. href = '%s/%s/%s' % (linkto, tablename, r_old)
  2545. r = A(r, _href=href)
  2546. elif isinstance(field.type, str) and field.type.startswith('reference'):
  2547. if linkto:
  2548. ref = field.type[10:]
  2549. try:
  2550. href = linkto(r, 'reference', ref)
  2551. except TypeError:
  2552. href = '%s/%s/%s' % (linkto, ref, r_old)
  2553. if ref.find('.') >= 0:
  2554. tref, fref = ref.split('.')
  2555. if hasattr(sqlrows.db[tref], '_primarykey'):
  2556. href = '%s/%s?%s' % (linkto, tref, urllib.urlencode({fref: r}))
  2557. r = A(represent(field, r, record), _href=str(href))
  2558. elif field.represent:
  2559. r = represent(field, r, record)
  2560. elif linkto and hasattr(field._table, '_primarykey')\
  2561. and fieldname in field._table._primarykey:
  2562. # have to test this with multi-key tables
  2563. key = urllib.urlencode(dict([
  2564. ((tablename in record
  2565. and isinstance(record, Row)
  2566. and isinstance(record[tablename], Row)) and
  2567. (k, record[tablename][k])) or (k, record[k])
  2568. for k in field._table._primarykey]))
  2569. r = A(r, _href='%s/%s?%s' % (linkto, tablename, key))
  2570. elif isinstance(field.type, str) and field.type.startswith('list:'):
  2571. r = represent(field, r or [], record)
  2572. elif field.represent:
  2573. r = represent(field, r, record)
  2574. elif field.type == 'blob' and r:
  2575. r = 'DATA'
  2576. elif field.type == 'upload':
  2577. if upload and r:
  2578. r = A(current.T('file'), _href='%s/%s' % (upload, r))
  2579. elif r:
  2580. r = current.T('file')
  2581. else:
  2582. r = ''
  2583. elif field.type in ['string', 'text']:
  2584. r = str(field.formatter(r))
  2585. if headers != {}: # new implement dict
  2586. if isinstance(headers[colname], dict):
  2587. if isinstance(headers[colname]['truncate'], int):
  2588. r = truncate_string(
  2589. r, headers[colname]['truncate'])
  2590. elif not truncate is None:
  2591. r = truncate_string(r, truncate)
  2592. attrcol = dict() # new implement dict
  2593. if headers != {}:
  2594. if isinstance(headers[colname], dict):
  2595. colclass = headers[colname]['class']
  2596. if headers[colname]['selected']:
  2597. colclass = str(headers[colname]
  2598. ['class'] + " colselected").strip()
  2599. if colclass != "":
  2600. attrcol.update(_class=colclass)
  2601. row.append(TD(r, **attrcol))
  2602. if extracolumns: # new implement dict
  2603. for c in extracolumns:
  2604. attrcol = dict()
  2605. colclass = c['class']
  2606. if c['selected']:
  2607. colclass = str(c['class'] + " colselected").strip()
  2608. if colclass != "":
  2609. attrcol.update(_class=colclass)
  2610. contentfunc = c['content']
  2611. row.append(TD(contentfunc(record, rc), **attrcol))
  2612. tbody.append(TR(_class=_class, *row))
  2613. if renderstyle:
  2614. components.append(STYLE(self.style()))
  2615. components.append(TBODY(*tbody))
  2616. def style(self):
  2617. css = '''
  2618. table tbody tr.odd {
  2619. background-color: #DFD;
  2620. }
  2621. table tbody tr.even {
  2622. background-color: #EFE;
  2623. }
  2624. table tbody tr.rowselected {
  2625. background-color: #FDD;
  2626. }
  2627. table tbody tr td.colselected {
  2628. background-color: #FDD;
  2629. }
  2630. table tbody tr:hover {
  2631. background: #DDF;
  2632. }
  2633. '''
  2634. return css
  2635. form_factory = SQLFORM.factory # for backward compatibility, deprecated
  2636. class ExportClass(object):
  2637. label = None
  2638. file_ext = None
  2639. content_type = None
  2640. def __init__(self, rows):
  2641. self.rows = rows
  2642. def represented(self):
  2643. def none_exception(value):
  2644. """
  2645. returns a cleaned up value that can be used for csv export:
  2646. - unicode text is encoded as such
  2647. - None values are replaced with the given representation (default <NULL>)
  2648. """
  2649. if value is None:
  2650. return '<NULL>'
  2651. elif isinstance(value, unicode):
  2652. return value.encode('utf8')
  2653. elif isinstance(value, Reference):
  2654. return int(value)
  2655. elif hasattr(value, 'isoformat'):
  2656. return value.isoformat()[:19].replace('T', ' ')
  2657. elif isinstance(value, (list, tuple)): # for type='list:..'
  2658. return bar_encode(value)
  2659. return value
  2660. represented = []
  2661. for record in self.rows:
  2662. row = []
  2663. for col in self.rows.colnames:
  2664. if not REGEX_TABLE_DOT_FIELD.match(col):
  2665. row.append(record._extra[col])
  2666. else:
  2667. (t, f) = col.split('.')
  2668. field = self.rows.db[t][f]
  2669. if isinstance(record.get(t, None), (Row, dict)):
  2670. value = record[t][f]
  2671. else:
  2672. value = record[f]
  2673. if field.type == 'blob' and not value is None:
  2674. value = ''
  2675. elif field.represent:
  2676. value = field.represent(value, record)
  2677. row.append(none_exception(value))
  2678. represented.append(row)
  2679. return represented
  2680. def export(self):
  2681. raise NotImplementedError
  2682. class ExporterTSV(ExportClass):
  2683. label = 'TSV'
  2684. file_ext = "csv"
  2685. content_type = "text/tab-separated-values"
  2686. def __init__(self, rows):
  2687. ExportClass.__init__(self, rows)
  2688. def export(self):
  2689. out = cStringIO.StringIO()
  2690. final = cStringIO.StringIO()
  2691. import csv
  2692. writer = csv.writer(out, delimiter='\t')
  2693. if self.rows:
  2694. import codecs
  2695. final.write(codecs.BOM_UTF16)
  2696. writer.writerow(
  2697. [unicode(col).encode("utf8") for col in self.rows.colnames])
  2698. data = out.getvalue().decode("utf8")
  2699. data = data.encode("utf-16")
  2700. data = data[2:]
  2701. final.write(data)
  2702. out.truncate(0)
  2703. records = self.represented()
  2704. for row in records:
  2705. writer.writerow(
  2706. [str(col).decode('utf8').encode("utf-8") for col in row])
  2707. data = out.getvalue().decode("utf8")
  2708. data = data.encode("utf-16")
  2709. data = data[2:]
  2710. final.write(data)
  2711. out.truncate(0)
  2712. return str(final.getvalue())
  2713. class ExporterCSV(ExportClass):
  2714. label = 'CSV'
  2715. file_ext = "csv"
  2716. content_type = "text/csv"
  2717. def __init__(self, rows):
  2718. ExportClass.__init__(self, rows)
  2719. def export(self):
  2720. if self.rows:
  2721. return self.rows.as_csv()
  2722. else:
  2723. return ''
  2724. class ExporterHTML(ExportClass):
  2725. label = 'HTML'
  2726. file_ext = "html"
  2727. content_type = "text/html"
  2728. def __init__(self, rows):
  2729. ExportClass.__init__(self, rows)
  2730. def export(self):
  2731. if self.rows:
  2732. return self.rows.xml()
  2733. else:
  2734. return '<html>\n<body>\n<table>\n</table>\n</body>\n</html>'
  2735. class ExporterXML(ExportClass):
  2736. label = 'XML'
  2737. file_ext = "xml"
  2738. content_type = "text/xml"
  2739. def __init__(self, rows):
  2740. ExportClass.__init__(self, rows)
  2741. def export(self):
  2742. if self.rows:
  2743. return self.rows.as_xml()
  2744. else:
  2745. return '<rows></rows>'
  2746. class ExporterJSON(ExportClass):
  2747. label = 'JSON'
  2748. file_ext = "json"
  2749. content_type = "application/json"
  2750. def __init__(self, rows):
  2751. ExportClass.__init__(self, rows)
  2752. def export(self):
  2753. if self.rows:
  2754. return self.rows.as_json()
  2755. else:
  2756. return 'null'