1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | """ |
---|
5 | | This file is part of the web2py Web Framework |
---|
6 | | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> |
---|
7 | | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
8 | |
---|
9 | Holds: |
---|
10 | |
---|
11 | - SQLFORM: provide a form for a table (with/without record) |
---|
12 | - SQLTABLE: provides a table for a set of records |
---|
13 | - form_factory: provides a SQLFORM for an non-db backed table |
---|
14 | |
---|
15 | """ |
---|
16 | |
---|
17 | import datetime |
---|
18 | import re |
---|
19 | import copy |
---|
20 | |
---|
21 | import os |
---|
22 | from gluon._compat import StringIO,unichr, urllib_quote, iteritems, basestring, long, integer_types, unicodeT, to_native, to_unicode, urlencode |
---|
23 | from gluon.http import HTTP, redirect |
---|
24 | from gluon.html import XmlComponent, truncate_string |
---|
25 | from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG |
---|
26 | from gluon.html import FORM, INPUT, LABEL, OPTION, SELECT, COL, COLGROUP |
---|
27 | from gluon.html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE, SCRIPT |
---|
28 | from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY |
---|
29 | from pydal.base import DEFAULT |
---|
30 | from pydal.objects import Table, Row, Expression, Field, Set, Rows |
---|
31 | from pydal.adapters.base import CALLABLETYPES |
---|
32 | from pydal.helpers.methods import smart_query, bar_encode, _repr_ref, merge_tablemaps |
---|
33 | from pydal.helpers.classes import Reference, SQLCustomType |
---|
34 | from pydal.default_validators import default_validators |
---|
35 | from gluon.storage import Storage |
---|
36 | from gluon.utils import md5_hash |
---|
37 | from gluon.validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE |
---|
38 | from gluon.validators import IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE |
---|
39 | from gluon.validators import IS_STRONG |
---|
40 | |
---|
41 | import gluon.serializers as serializers |
---|
42 | from gluon.globals import current |
---|
43 | from functools import reduce |
---|
44 | |
---|
45 | try: |
---|
46 | import gluon.settings as settings |
---|
47 | except ImportError: |
---|
48 | settings = {} |
---|
49 | |
---|
50 | |
---|
51 | REGEX_WIDGET_CLASS = re.compile(r'^\w*') |
---|
52 | |
---|
53 | |
---|
54 | def add_class(a, b): |
---|
55 | return a + ' ' + b if a else b |
---|
56 | |
---|
57 | def count_expected_args(f): |
---|
58 | if hasattr(f,'func_code'): |
---|
59 | # python 2 |
---|
60 | n = f.func_code.co_argcount - len(f.func_defaults or []) |
---|
61 | if getattr(f, 'im_self', None): |
---|
62 | n -= 1 |
---|
63 | elif hasattr(f, '__code__'): |
---|
64 | # python 3 |
---|
65 | n = f.__code__.co_argcount - len(f.__defaults__ or []) |
---|
66 | if getattr(f, '__self__', None): |
---|
67 | n -= 1 |
---|
68 | else: |
---|
69 | # doh! |
---|
70 | n = 1 |
---|
71 | return n |
---|
72 | |
---|
73 | def represent(field, value, record): |
---|
74 | f = field.represent |
---|
75 | if not callable(f): |
---|
76 | return str(value) |
---|
77 | n = count_expected_args(f) |
---|
78 | if n == 1: |
---|
79 | return f(value) |
---|
80 | elif n == 2: |
---|
81 | return f(value, record) |
---|
82 | else: |
---|
83 | raise RuntimeError("field representation must take 1 or 2 args") |
---|
84 | |
---|
85 | class CacheRepresenter(object): |
---|
86 | def __init__(self): |
---|
87 | self.cache = {} |
---|
88 | def __call__(self, field, value, row): |
---|
89 | cache = self.cache |
---|
90 | if field not in cache: |
---|
91 | cache[field] = {} |
---|
92 | try: |
---|
93 | nvalue = cache[field][value] |
---|
94 | except KeyError: |
---|
95 | try: |
---|
96 | nvalue = field.represent(value, row) |
---|
97 | except KeyError: |
---|
98 | try: |
---|
99 | nvalue = field.represent(value, row[field.tablename]) |
---|
100 | except KeyError: |
---|
101 | nvalue = None |
---|
102 | if isinstance(field, _repr_ref): |
---|
103 | cache[field][value] = nvalue |
---|
104 | return nvalue |
---|
105 | |
---|
106 | |
---|
107 | def safe_int(x, i=0): |
---|
108 | try: |
---|
109 | return int(x) |
---|
110 | except (ValueError, TypeError): |
---|
111 | return i |
---|
112 | |
---|
113 | |
---|
114 | def safe_float(x): |
---|
115 | try: |
---|
116 | return float(x) |
---|
117 | except (ValueError, TypeError): |
---|
118 | return 0 |
---|
119 | |
---|
120 | |
---|
121 | def show_if(cond): |
---|
122 | if not cond: |
---|
123 | return None |
---|
124 | base = "%s_%s" % (cond.first.tablename, cond.first.name) |
---|
125 | if ((cond.op.__name__ == 'eq' and cond.second is True) or |
---|
126 | (cond.op.__name__ == 'ne' and cond.second is False)): |
---|
127 | return base, ":checked" |
---|
128 | if ((cond.op.__name__ == 'eq' and cond.second is False) or |
---|
129 | (cond.op.__name__ == 'ne' and cond.second is True)): |
---|
130 | return base, ":not(:checked)" |
---|
131 | if cond.op.__name__ == 'eq': |
---|
132 | return base, "[value='%s']" % cond.second |
---|
133 | if cond.op.__name__ == 'ne': |
---|
134 | return base, "[value!='%s']" % cond.second |
---|
135 | if cond.op.__name__ == 'contains': |
---|
136 | return base, "[value~='%s']" % cond.second |
---|
137 | if cond.op.__name__ == 'belongs': |
---|
138 | if isinstance(cond.second, set): |
---|
139 | cond.second = list(cond.second) |
---|
140 | if isinstance(cond.second, (list, tuple)): |
---|
141 | return base, ','.join("[value='%s']" % (v) for v in cond.second) |
---|
142 | raise RuntimeError("Not Implemented Error") |
---|
143 | |
---|
144 | |
---|
145 | PLURALIZE_RULES = None |
---|
146 | |
---|
147 | def pluralize(singular, rules=None): |
---|
148 | if rules is None: |
---|
149 | global PLURALIZE_RULES |
---|
150 | if PLURALIZE_RULES is None: |
---|
151 | PLURALIZE_RULES = [ |
---|
152 | (re.compile('child$'), re.compile('child$'), 'children'), |
---|
153 | (re.compile('oot$'), re.compile('oot$'), 'eet'), |
---|
154 | (re.compile('ooth$'), re.compile('ooth$'), 'eeth'), |
---|
155 | (re.compile('l[eo]af$'), re.compile('l([eo])af$'), 'l\\1aves'), |
---|
156 | (re.compile('sis$'), re.compile('sis$'), 'ses'), |
---|
157 | (re.compile('man$'), re.compile('man$'), 'men'), |
---|
158 | (re.compile('ife$'), re.compile('ife$'), 'ives'), |
---|
159 | (re.compile('eau$'), re.compile('eau$'), 'eaux'), |
---|
160 | (re.compile('lf$'), re.compile('lf$'), 'lves'), |
---|
161 | (re.compile('[sxz]$'), re.compile('$'), 'es'), |
---|
162 | (re.compile('[^aeioudgkprt]h$'), re.compile('$'), 'es'), |
---|
163 | (re.compile('(qu|[^aeiou])y$'), re.compile('y$'), 'ies'), |
---|
164 | (re.compile('$'), re.compile('$'), 's'), |
---|
165 | ] |
---|
166 | rules = PLURALIZE_RULES |
---|
167 | for line in rules: |
---|
168 | re_search, re_sub, replace = line |
---|
169 | plural = re_search.search(singular) and re_sub.sub(replace, singular) |
---|
170 | if plural: return plural |
---|
171 | |
---|
172 | |
---|
173 | class FormWidget(object): |
---|
174 | """ |
---|
175 | Helper for SQLFORM to generate form input fields (widget), related to the |
---|
176 | fieldtype |
---|
177 | """ |
---|
178 | |
---|
179 | _class = 'generic-widget' |
---|
180 | |
---|
181 | @classmethod |
---|
182 | def _attributes(cls, field, |
---|
183 | widget_attributes, **attributes): |
---|
184 | """ |
---|
185 | Helper to build a common set of attributes |
---|
186 | |
---|
187 | Args: |
---|
188 | field: the field involved, some attributes are derived from this |
---|
189 | widget_attributes: widget related attributes |
---|
190 | attributes: any other supplied attributes |
---|
191 | """ |
---|
192 | attr = dict( |
---|
193 | _id='%s_%s' % (field.tablename, field.name), |
---|
194 | _class=cls._class or |
---|
195 | REGEX_WIDGET_CLASS.match(str(field.type)).group(), |
---|
196 | _name=field.name, |
---|
197 | requires=field.requires, |
---|
198 | ) |
---|
199 | if getattr(field, 'show_if', None): |
---|
200 | trigger, cond = show_if(field.show_if) |
---|
201 | attr['_data-show-trigger'] = trigger |
---|
202 | attr['_data-show-if'] = cond |
---|
203 | attr.update(widget_attributes) |
---|
204 | attr.update(attributes) |
---|
205 | return attr |
---|
206 | |
---|
207 | @classmethod |
---|
208 | def widget(cls, field, value, **attributes): |
---|
209 | """ |
---|
210 | Generates the widget for the field. |
---|
211 | |
---|
212 | When serialized, will provide an INPUT tag: |
---|
213 | |
---|
214 | - id = tablename_fieldname |
---|
215 | - class = field.type |
---|
216 | - name = fieldname |
---|
217 | |
---|
218 | Args: |
---|
219 | field: the field needing the widget |
---|
220 | value: value |
---|
221 | attributes: any other attributes to be applied |
---|
222 | """ |
---|
223 | |
---|
224 | raise NotImplementedError |
---|
225 | |
---|
226 | |
---|
227 | class StringWidget(FormWidget): |
---|
228 | _class = 'string' |
---|
229 | |
---|
230 | @classmethod |
---|
231 | def widget(cls, field, value, **attributes): |
---|
232 | """ |
---|
233 | Generates an INPUT text tag. |
---|
234 | |
---|
235 | see also: `FormWidget.widget` |
---|
236 | """ |
---|
237 | |
---|
238 | default = dict( |
---|
239 | _type='text', |
---|
240 | value=(value is not None and str(value)) or '', |
---|
241 | ) |
---|
242 | attr = cls._attributes(field, default, **attributes) |
---|
243 | |
---|
244 | return INPUT(**attr) |
---|
245 | |
---|
246 | |
---|
247 | class IntegerWidget(StringWidget): |
---|
248 | _class = 'integer' |
---|
249 | |
---|
250 | |
---|
251 | class DoubleWidget(StringWidget): |
---|
252 | _class = 'double' |
---|
253 | |
---|
254 | |
---|
255 | class DecimalWidget(StringWidget): |
---|
256 | _class = 'decimal' |
---|
257 | |
---|
258 | |
---|
259 | class TimeWidget(StringWidget): |
---|
260 | _class = 'time' |
---|
261 | |
---|
262 | |
---|
263 | class DateWidget(StringWidget): |
---|
264 | _class = 'date' |
---|
265 | |
---|
266 | |
---|
267 | class DatetimeWidget(StringWidget): |
---|
268 | _class = 'datetime' |
---|
269 | |
---|
270 | |
---|
271 | class TextWidget(FormWidget): |
---|
272 | _class = 'text' |
---|
273 | |
---|
274 | @classmethod |
---|
275 | def widget(cls, field, value, **attributes): |
---|
276 | """ |
---|
277 | Generates a TEXTAREA tag. |
---|
278 | |
---|
279 | see also: `FormWidget.widget` |
---|
280 | """ |
---|
281 | |
---|
282 | default = dict(value=value) |
---|
283 | attr = cls._attributes(field, default, **attributes) |
---|
284 | return TEXTAREA(**attr) |
---|
285 | |
---|
286 | |
---|
287 | class JSONWidget(FormWidget): |
---|
288 | _class = 'json' |
---|
289 | |
---|
290 | @classmethod |
---|
291 | def widget(cls, field, value, **attributes): |
---|
292 | """ |
---|
293 | Generates a TEXTAREA for JSON notation. |
---|
294 | |
---|
295 | see also: `FormWidget.widget` |
---|
296 | """ |
---|
297 | if not isinstance(value, basestring): |
---|
298 | if value is not None: |
---|
299 | value = serializers.json(value) |
---|
300 | default = dict(value=value) |
---|
301 | attr = cls._attributes(field, default, **attributes) |
---|
302 | return TEXTAREA(**attr) |
---|
303 | |
---|
304 | |
---|
305 | class BooleanWidget(FormWidget): |
---|
306 | _class = 'boolean' |
---|
307 | |
---|
308 | @classmethod |
---|
309 | def widget(cls, field, value, **attributes): |
---|
310 | """ |
---|
311 | Generates an INPUT checkbox tag. |
---|
312 | |
---|
313 | see also: `FormWidget.widget` |
---|
314 | """ |
---|
315 | |
---|
316 | default = dict(_type='checkbox', value=value) |
---|
317 | attr = cls._attributes(field, default, |
---|
318 | **attributes) |
---|
319 | return INPUT(**attr) |
---|
320 | |
---|
321 | |
---|
322 | class OptionsWidget(FormWidget): |
---|
323 | |
---|
324 | @staticmethod |
---|
325 | def has_options(field): |
---|
326 | """ |
---|
327 | Checks if the field has selectable options |
---|
328 | |
---|
329 | Args: |
---|
330 | field: the field needing checking |
---|
331 | |
---|
332 | Returns: |
---|
333 | True if the field has options |
---|
334 | """ |
---|
335 | |
---|
336 | return hasattr(field.requires, 'options') |
---|
337 | |
---|
338 | @classmethod |
---|
339 | def widget(cls, field, value, **attributes): |
---|
340 | """ |
---|
341 | Generates a SELECT tag, including OPTIONs (only 1 option allowed) |
---|
342 | |
---|
343 | see also: `FormWidget.widget` |
---|
344 | """ |
---|
345 | default = dict(value=value) |
---|
346 | attr = cls._attributes(field, default, |
---|
347 | **attributes) |
---|
348 | requires = field.requires |
---|
349 | if not isinstance(requires, (list, tuple)): |
---|
350 | requires = [requires] |
---|
351 | if requires: |
---|
352 | if hasattr(requires[0], 'options'): |
---|
353 | options = requires[0].options() |
---|
354 | else: |
---|
355 | raise SyntaxError( |
---|
356 | 'widget cannot determine options of %s' % field) |
---|
357 | opts = [OPTION(v, _value=k) for (k, v) in options] |
---|
358 | return SELECT(*opts, **attr) |
---|
359 | |
---|
360 | |
---|
361 | class ListWidget(StringWidget): |
---|
362 | |
---|
363 | @classmethod |
---|
364 | def widget(cls, field, value, **attributes): |
---|
365 | _id = '%s_%s' % (field.tablename, field.name) |
---|
366 | _name = field.name |
---|
367 | if field.type == 'list:integer': |
---|
368 | _class = 'integer' |
---|
369 | else: |
---|
370 | _class = 'string' |
---|
371 | requires = field.requires if isinstance( |
---|
372 | field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None |
---|
373 | if isinstance(value, str): |
---|
374 | value = [value] |
---|
375 | nvalue = value or [''] |
---|
376 | items = [LI(INPUT(_id=_id, _class=_class, _name=_name, |
---|
377 | value=v, hideerror=k < len(nvalue) - 1, |
---|
378 | requires=requires), |
---|
379 | **attributes) for (k, v) in enumerate(nvalue)] |
---|
380 | attributes['_id'] = _id + '_grow_input' |
---|
381 | attributes['_style'] = 'list-style:none' |
---|
382 | attributes['_class'] = 'w2p_list' |
---|
383 | return UL(*items, **attributes) |
---|
384 | |
---|
385 | |
---|
386 | class MultipleOptionsWidget(OptionsWidget): |
---|
387 | |
---|
388 | @classmethod |
---|
389 | def widget(cls, field, value, size=5, **attributes): |
---|
390 | """ |
---|
391 | Generates a SELECT tag, including OPTIONs (multiple options allowed) |
---|
392 | |
---|
393 | see also: `FormWidget.widget` |
---|
394 | |
---|
395 | Args: |
---|
396 | size: optional param (default=5) to indicate how many rows must |
---|
397 | be shown |
---|
398 | """ |
---|
399 | |
---|
400 | attributes.update(_size=size, _multiple=True) |
---|
401 | |
---|
402 | return OptionsWidget.widget(field, value, **attributes) |
---|
403 | |
---|
404 | |
---|
405 | class RadioWidget(OptionsWidget): |
---|
406 | |
---|
407 | @classmethod |
---|
408 | def widget(cls, field, value, **attributes): |
---|
409 | """ |
---|
410 | Generates a TABLE tag, including INPUT radios (only 1 option allowed) |
---|
411 | |
---|
412 | see also: `FormWidget.widget` |
---|
413 | """ |
---|
414 | |
---|
415 | if isinstance(value, (list, tuple)): |
---|
416 | value = str(value[0]) |
---|
417 | else: |
---|
418 | value = str(value) |
---|
419 | |
---|
420 | attr = cls._attributes(field, {}, **attributes) |
---|
421 | attr['_class'] = add_class(attr.get('_class'), 'web2py_radiowidget') |
---|
422 | |
---|
423 | requires = field.requires |
---|
424 | if not isinstance(requires, (list, tuple)): |
---|
425 | requires = [requires] |
---|
426 | if requires: |
---|
427 | if hasattr(requires[0], 'options'): |
---|
428 | options = requires[0].options() |
---|
429 | else: |
---|
430 | raise SyntaxError('widget cannot determine options of %s' |
---|
431 | % field) |
---|
432 | options = [(k, v) for k, v in options if str(v)] |
---|
433 | opts = [] |
---|
434 | cols = attributes.get('cols', 1) |
---|
435 | totals = len(options) |
---|
436 | mods = totals % cols |
---|
437 | rows = totals // cols |
---|
438 | if mods: |
---|
439 | rows += 1 |
---|
440 | |
---|
441 | # widget style |
---|
442 | wrappers = dict( |
---|
443 | table=(TABLE, TR, TD), |
---|
444 | ul=(DIV, UL, LI), |
---|
445 | divs=(DIV, DIV, DIV) |
---|
446 | ) |
---|
447 | parent, child, inner = wrappers[attributes.get('style', 'table')] |
---|
448 | |
---|
449 | for r_index in range(rows): |
---|
450 | tds = [] |
---|
451 | for k, v in options[r_index * cols:(r_index + 1) * cols]: |
---|
452 | checked = {'_checked': 'checked'} if k == value else {} |
---|
453 | tds.append(inner(INPUT(_type='radio', |
---|
454 | _id='%s%s' % (field.name, k), |
---|
455 | _name=field.name, |
---|
456 | requires=attr.get('requires', None), |
---|
457 | hideerror=True, _value=k, |
---|
458 | value=value, |
---|
459 | **checked), |
---|
460 | LABEL(v, _for='%s%s' % (field.name, k)))) |
---|
461 | opts.append(child(tds)) |
---|
462 | |
---|
463 | if opts: |
---|
464 | opts.append( |
---|
465 | INPUT(requires=attr.get('requires', None), |
---|
466 | _style="display:none;", |
---|
467 | _disabled="disabled", |
---|
468 | _name=field.name, |
---|
469 | hideerror=False)) |
---|
470 | return parent(*opts, **attr) |
---|
471 | |
---|
472 | |
---|
473 | class CheckboxesWidget(OptionsWidget): |
---|
474 | |
---|
475 | @classmethod |
---|
476 | def widget(cls, field, value, **attributes): |
---|
477 | """ |
---|
478 | Generates a TABLE tag, including INPUT checkboxes (multiple allowed) |
---|
479 | |
---|
480 | see also: `FormWidget.widget` |
---|
481 | """ |
---|
482 | |
---|
483 | # was values = re.compile('[\w\-:]+').findall(str(value)) |
---|
484 | if isinstance(value, (list, tuple)): |
---|
485 | values = [str(v) for v in value] |
---|
486 | else: |
---|
487 | values = [str(value)] |
---|
488 | |
---|
489 | attr = cls._attributes(field, {}, **attributes) |
---|
490 | attr['_class'] = add_class(attr.get('_class'), 'web2py_checkboxeswidget') |
---|
491 | |
---|
492 | label = attr.get('label', True) |
---|
493 | |
---|
494 | requires = field.requires |
---|
495 | if not isinstance(requires, (list, tuple)): |
---|
496 | requires = [requires] |
---|
497 | if requires and hasattr(requires[0], 'options'): |
---|
498 | options = requires[0].options() |
---|
499 | else: |
---|
500 | raise SyntaxError('widget cannot determine options of %s' |
---|
501 | % field) |
---|
502 | |
---|
503 | options = [(k, v) for k, v in options if k != ''] |
---|
504 | opts = [] |
---|
505 | cols = attributes.get('cols', 1) |
---|
506 | totals = len(options) |
---|
507 | mods = totals % cols |
---|
508 | rows = totals // cols |
---|
509 | if mods: |
---|
510 | rows += 1 |
---|
511 | |
---|
512 | # widget style |
---|
513 | wrappers = dict( |
---|
514 | table=(TABLE, TR, TD), |
---|
515 | ul=(DIV, UL, LI), |
---|
516 | divs=(DIV, DIV, DIV) |
---|
517 | ) |
---|
518 | parent, child, inner = wrappers[attributes.get('style', 'table')] |
---|
519 | |
---|
520 | for r_index in range(rows): |
---|
521 | tds = [] |
---|
522 | for k, v in options[r_index * cols:(r_index + 1) * cols]: |
---|
523 | if k in values: |
---|
524 | r_value = k |
---|
525 | else: |
---|
526 | r_value = [] |
---|
527 | tds.append(inner(INPUT(_type='checkbox', |
---|
528 | _id='%s%s' % (field.name, k), |
---|
529 | _name=field.name, |
---|
530 | requires=attr.get('requires', None), |
---|
531 | hideerror=True, _value=k, |
---|
532 | value=r_value), |
---|
533 | LABEL(v, _for='%s%s' % (field.name, k)) |
---|
534 | if label else '')) |
---|
535 | opts.append(child(tds)) |
---|
536 | |
---|
537 | if opts: |
---|
538 | opts.append( |
---|
539 | INPUT(requires=attr.get('requires', None), |
---|
540 | _style="display:none;", |
---|
541 | _disabled="disabled", |
---|
542 | _name=field.name, |
---|
543 | hideerror=False)) |
---|
544 | return parent(*opts, **attr) |
---|
545 | |
---|
546 | |
---|
547 | class PasswordWidget(FormWidget): |
---|
548 | _class = 'password' |
---|
549 | |
---|
550 | @classmethod |
---|
551 | def widget(cls, field, value, **attributes): |
---|
552 | """ |
---|
553 | Generates a INPUT password tag. |
---|
554 | If a value is present it will be shown as a number of '*', not related |
---|
555 | to the length of the actual value. |
---|
556 | |
---|
557 | see also: `FormWidget.widget` |
---|
558 | """ |
---|
559 | # detect if attached a IS_STRONG with entropy |
---|
560 | default = dict( |
---|
561 | _type='password', |
---|
562 | _value=(value and DEFAULT_PASSWORD_DISPLAY) or '', |
---|
563 | ) |
---|
564 | attr = cls._attributes(field, default, **attributes) |
---|
565 | |
---|
566 | # deal with entropy check! |
---|
567 | requires = field.requires |
---|
568 | if not isinstance(requires, (list, tuple)): |
---|
569 | requires = [requires] |
---|
570 | is_strong = [r for r in requires if isinstance(r, IS_STRONG)] |
---|
571 | if is_strong: |
---|
572 | attr['_data-w2p_entropy'] = is_strong[0].entropy if is_strong[0].entropy else "null" |
---|
573 | # end entropy check |
---|
574 | output = INPUT(**attr) |
---|
575 | return output |
---|
576 | |
---|
577 | |
---|
578 | class UploadWidget(FormWidget): |
---|
579 | _class = 'upload' |
---|
580 | |
---|
581 | DEFAULT_WIDTH = '150px' |
---|
582 | ID_DELETE_SUFFIX = '__delete' |
---|
583 | GENERIC_DESCRIPTION = 'file ## download' |
---|
584 | DELETE_FILE = 'delete' |
---|
585 | |
---|
586 | @classmethod |
---|
587 | def widget(cls, field, value, download_url=None, **attributes): |
---|
588 | """ |
---|
589 | generates a INPUT file tag. |
---|
590 | |
---|
591 | Optionally provides an A link to the file, including a checkbox so |
---|
592 | the file can be deleted. |
---|
593 | |
---|
594 | All is wrapped in a DIV. |
---|
595 | |
---|
596 | see also: `FormWidget.widget` |
---|
597 | |
---|
598 | Args: |
---|
599 | field: the field |
---|
600 | value: the field value |
---|
601 | download_url: url for the file download (default = None) |
---|
602 | """ |
---|
603 | |
---|
604 | default = dict(_type='file', ) |
---|
605 | attr = cls._attributes(field, default, **attributes) |
---|
606 | |
---|
607 | inp = INPUT(**attr) |
---|
608 | |
---|
609 | if download_url and value: |
---|
610 | if callable(download_url): |
---|
611 | url = download_url(value) |
---|
612 | else: |
---|
613 | url = download_url + '/' + value |
---|
614 | (br, image) = ('', '') |
---|
615 | if UploadWidget.is_image(value): |
---|
616 | br = BR() |
---|
617 | image = IMG(_src=url, _width=cls.DEFAULT_WIDTH) |
---|
618 | |
---|
619 | requires = attr["requires"] |
---|
620 | if requires == [] or isinstance(requires, IS_EMPTY_OR): |
---|
621 | inp = DIV(inp, |
---|
622 | SPAN('[', |
---|
623 | A(current.T( |
---|
624 | UploadWidget.GENERIC_DESCRIPTION), _href=url), |
---|
625 | '|', |
---|
626 | INPUT(_type='checkbox', |
---|
627 | _name=field.name + cls.ID_DELETE_SUFFIX, |
---|
628 | _id=field.name + cls.ID_DELETE_SUFFIX), |
---|
629 | LABEL(current.T(cls.DELETE_FILE), |
---|
630 | _for=field.name + cls.ID_DELETE_SUFFIX, |
---|
631 | _style='display:inline'), |
---|
632 | ']', _style='white-space:nowrap'), |
---|
633 | br, image) |
---|
634 | else: |
---|
635 | inp = DIV(inp, |
---|
636 | SPAN('[', |
---|
637 | A(current.T(cls.GENERIC_DESCRIPTION), _href=url), |
---|
638 | ']', _style='white-space:nowrap'), |
---|
639 | br, image) |
---|
640 | return inp |
---|
641 | |
---|
642 | @classmethod |
---|
643 | def represent(cls, field, value, download_url=None): |
---|
644 | """ |
---|
645 | How to represent the file: |
---|
646 | |
---|
647 | - with download url and if it is an image: <A href=...><IMG ...></A> |
---|
648 | - otherwise with download url: <A href=...>file</A> |
---|
649 | - otherwise: file |
---|
650 | |
---|
651 | Args: |
---|
652 | field: the field |
---|
653 | value: the field value |
---|
654 | download_url: url for the file download (default = None) |
---|
655 | """ |
---|
656 | |
---|
657 | inp = current.T(cls.GENERIC_DESCRIPTION) |
---|
658 | |
---|
659 | if download_url and value: |
---|
660 | if callable(download_url): |
---|
661 | url = download_url(value) |
---|
662 | else: |
---|
663 | url = download_url + '/' + value |
---|
664 | if cls.is_image(value): |
---|
665 | inp = IMG(_src=url, _width=cls.DEFAULT_WIDTH) |
---|
666 | inp = A(inp, _href=url) |
---|
667 | |
---|
668 | return inp |
---|
669 | |
---|
670 | @staticmethod |
---|
671 | def is_image(value): |
---|
672 | """ |
---|
673 | Tries to check if the filename provided references to an image |
---|
674 | |
---|
675 | Checking is based on filename extension. Currently recognized: |
---|
676 | gif, png, jp(e)g, bmp |
---|
677 | |
---|
678 | Args: |
---|
679 | value: filename |
---|
680 | """ |
---|
681 | |
---|
682 | extension = value.split('.')[-1].lower() |
---|
683 | if extension in ['gif', 'png', 'jpg', 'jpeg', 'bmp']: |
---|
684 | return True |
---|
685 | return False |
---|
686 | |
---|
687 | |
---|
688 | class AutocompleteWidget(object): |
---|
689 | _class = 'string' |
---|
690 | |
---|
691 | def __init__(self, request, field, id_field=None, db=None, |
---|
692 | orderby=None, limitby=(0, 10), distinct=False, |
---|
693 | keyword='_autocomplete_%(tablename)s_%(fieldname)s', |
---|
694 | min_length=2, help_fields=None, help_string=None, |
---|
695 | at_beginning=True, default_var='ac', user_signature=True, |
---|
696 | hash_vars=False): |
---|
697 | |
---|
698 | self.help_fields = help_fields or [] |
---|
699 | self.help_string = help_string |
---|
700 | if self.help_fields and not self.help_string: |
---|
701 | self.help_string = ' '.join('%%(%s)s' % f.name for f in self.help_fields) |
---|
702 | |
---|
703 | self.request = request |
---|
704 | self.keyword = keyword % dict(tablename=field.tablename, |
---|
705 | fieldname=field.name) |
---|
706 | self.db = db or field._db |
---|
707 | self.orderby = orderby |
---|
708 | self.limitby = limitby |
---|
709 | self.distinct = distinct |
---|
710 | self.min_length = min_length |
---|
711 | self.at_beginning = at_beginning |
---|
712 | self.fields = [field] |
---|
713 | if id_field: |
---|
714 | self.is_reference = True |
---|
715 | self.fields.append(id_field) |
---|
716 | else: |
---|
717 | self.is_reference = False |
---|
718 | if hasattr(request, 'application'): |
---|
719 | urlvars = copy.copy(request.vars) |
---|
720 | urlvars[default_var] = 1 |
---|
721 | self.url = URL(args=request.args, vars=urlvars, |
---|
722 | user_signature=user_signature, hash_vars=hash_vars) |
---|
723 | self.run_callback = True |
---|
724 | else: |
---|
725 | self.url = request |
---|
726 | self.run_callback = False |
---|
727 | |
---|
728 | def callback(self): |
---|
729 | if self.keyword in self.request.vars: |
---|
730 | field = self.fields[0] |
---|
731 | kword = self.request.vars[self.keyword] |
---|
732 | if isinstance(field, Field.Virtual): |
---|
733 | records = [] |
---|
734 | table_rows = self.db(self.db[field.tablename]).select(orderby=self.orderby) |
---|
735 | count = self.limitby[1] if self.limitby else -1 |
---|
736 | for row in table_rows: |
---|
737 | if self.at_beginning: |
---|
738 | if row[field.name].lower().startswith(kword): |
---|
739 | count -= 1 |
---|
740 | records.append(row) |
---|
741 | else: |
---|
742 | if kword in row[field.name].lower(): |
---|
743 | count -= 1 |
---|
744 | records.append(row) |
---|
745 | if count == 0: |
---|
746 | break |
---|
747 | if self.limitby and self.limitby[0]: |
---|
748 | records = records[self.limitby[0]:] |
---|
749 | rows = Rows(self.db, records, table_rows.colnames, |
---|
750 | compact=table_rows.compact) |
---|
751 | elif settings and settings.global_settings.web2py_runtime_gae: |
---|
752 | rows = self.db(field.__ge__(kword) & |
---|
753 | field.__lt__(kword + '\ufffd') |
---|
754 | ).select(orderby=self.orderby, |
---|
755 | limitby=self.limitby, |
---|
756 | *(self.fields + self.help_fields)) |
---|
757 | elif self.at_beginning: |
---|
758 | rows = self.db(field.like(kword + '%', case_sensitive=False) |
---|
759 | ).select(orderby=self.orderby, |
---|
760 | limitby=self.limitby, |
---|
761 | distinct=self.distinct, |
---|
762 | *(self.fields + self.help_fields)) |
---|
763 | else: |
---|
764 | rows = self.db(field.contains(kword, case_sensitive=False) |
---|
765 | ).select(orderby=self.orderby, |
---|
766 | limitby=self.limitby, |
---|
767 | distinct=self.distinct, |
---|
768 | *(self.fields + self.help_fields)) |
---|
769 | if rows: |
---|
770 | if self.is_reference: |
---|
771 | id_field = self.fields[1] |
---|
772 | if self.help_fields: |
---|
773 | options = [ |
---|
774 | OPTION( |
---|
775 | self.help_string % dict( |
---|
776 | [(h.name, s[h.name]) for h |
---|
777 | in self.fields[:1] + self.help_fields]), |
---|
778 | _value=s[id_field.name], |
---|
779 | _selected=(k == 0)) |
---|
780 | for k, s in enumerate(rows) |
---|
781 | ] |
---|
782 | else: |
---|
783 | options = [OPTION( |
---|
784 | s[field.name], _value=s[id_field.name], |
---|
785 | _selected=(k == 0)) for k, s in enumerate(rows)] |
---|
786 | raise HTTP( |
---|
787 | 200, SELECT(_id=self.keyword, _class='autocomplete', |
---|
788 | _size=len(rows), _multiple=(len(rows) == 1), |
---|
789 | *options).xml()) |
---|
790 | else: |
---|
791 | raise HTTP( |
---|
792 | 200, SELECT(_id=self.keyword, _class='autocomplete', |
---|
793 | _size=len(rows), _multiple=(len(rows) == 1), |
---|
794 | *[OPTION(s[field.name], |
---|
795 | _selected=(k == 0)) |
---|
796 | for k, s in enumerate(rows)]).xml()) |
---|
797 | else: |
---|
798 | raise HTTP(200, '') |
---|
799 | |
---|
800 | def __call__(self, field, value, **attributes): |
---|
801 | if self.run_callback: |
---|
802 | self.callback() |
---|
803 | default = dict( |
---|
804 | _type='text', |
---|
805 | value=(value is not None and str(value)) or '', |
---|
806 | ) |
---|
807 | attr = StringWidget._attributes(field, default, **attributes) |
---|
808 | div_id = self.keyword + '_div' |
---|
809 | attr['_autocomplete'] = 'off' |
---|
810 | if self.is_reference: |
---|
811 | key2 = self.keyword + '_aux' |
---|
812 | key3 = self.keyword + '_auto' |
---|
813 | attr['_class'] = 'string' |
---|
814 | name = attr['_name'] |
---|
815 | if 'requires' in attr: |
---|
816 | del attr['requires'] |
---|
817 | attr['_name'] = key2 |
---|
818 | value = attr['value'] |
---|
819 | if isinstance(self.fields[0], Field.Virtual): |
---|
820 | record = None |
---|
821 | table_rows = self.db(self.db[self.fields[0].tablename]).select(orderby=self.orderby) |
---|
822 | for row in table_rows: |
---|
823 | if row.id == value: |
---|
824 | record = row |
---|
825 | break |
---|
826 | else: |
---|
827 | record = self.db( |
---|
828 | self.fields[1] == value).select(self.fields[0]).first() |
---|
829 | attr['value'] = record and record[self.fields[0].name] |
---|
830 | attr['_onblur'] = "jQuery('#%(div_id)s').delay(500).fadeOut('slow');" % \ |
---|
831 | dict(div_id=div_id, u='F' + self.keyword) |
---|
832 | js = """ |
---|
833 | (function($) { |
---|
834 | function doit(e_) { |
---|
835 | $('#%(key3)s').val(''); |
---|
836 | var e=e_.which?e_.which:e_.keyCode; |
---|
837 | function %(u)s(){ |
---|
838 | $('#%(id)s').val($('#%(key)s option:selected').text()); |
---|
839 | $('#%(key3)s').val($('#%(key)s option:selected').val()) |
---|
840 | }; |
---|
841 | if(e==39) %(u)s(); |
---|
842 | else if(e==40) { |
---|
843 | if($('#%(key)s option:selected').next().length) |
---|
844 | $('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); |
---|
845 | %(u)s(); |
---|
846 | } |
---|
847 | else if(e==38) { |
---|
848 | if($('#%(key)s option:selected').prev().length) |
---|
849 | $('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); |
---|
850 | %(u)s(); |
---|
851 | } |
---|
852 | else if($('#%(id)s').val().length>=%(min_length)s) |
---|
853 | $.get('%(url)s&%(key)s='+encodeURIComponent($('#%(id)s').val()), |
---|
854 | function(data){ |
---|
855 | if(data=='')$('#%(key3)s').val(''); |
---|
856 | else{ |
---|
857 | $('#%(id)s').next('.error').hide(); |
---|
858 | $('#%(div_id)s').html(data).show().focus(); |
---|
859 | $('#%(div_id)s select').css('width',$('#%(id)s').css('width')); |
---|
860 | $('#%(key3)s').val($('#%(key)s option:selected').val()); |
---|
861 | $('#%(key)s').change(%(u)s).click(%(u)s); |
---|
862 | }; |
---|
863 | }); |
---|
864 | else $('#%(div_id)s').fadeOut('slow'); |
---|
865 | } |
---|
866 | var tmr = null; |
---|
867 | $("#%(id)s").on('keyup focus',function(e) { |
---|
868 | if (tmr) clearTimeout(tmr); |
---|
869 | if($('#%(id)s').val().length>=%(min_length)s) { |
---|
870 | tmr = setTimeout(function() { tmr = null; doit(e); }, 300); |
---|
871 | } |
---|
872 | }); |
---|
873 | })(jQuery)""".replace('\n', '').replace(' ' * 4, '') % \ |
---|
874 | dict(url=self.url, min_length=self.min_length, |
---|
875 | key=self.keyword, id=attr['_id'], key2=key2, key3=key3, |
---|
876 | name=name, div_id=div_id, u='F' + self.keyword) |
---|
877 | return CAT(INPUT(**attr), |
---|
878 | INPUT(_type='hidden', _id=key3, _value=value, |
---|
879 | _name=name, requires=field.requires), |
---|
880 | SCRIPT(js), |
---|
881 | DIV(_id=div_id, _style='position:absolute;')) |
---|
882 | else: |
---|
883 | attr['_name'] = field.name |
---|
884 | attr['_onblur'] = "jQuery('#%(div_id)s').delay(500).fadeOut('slow');" % \ |
---|
885 | dict(div_id=div_id, u='F' + self.keyword) |
---|
886 | js = """ |
---|
887 | (function($) { |
---|
888 | function doit(e_) { |
---|
889 | var e=e_.which?e_.which:e_.keyCode; |
---|
890 | function %(u)s(){ |
---|
891 | $('#%(id)s').val($('#%(key)s option:selected').val()) |
---|
892 | }; |
---|
893 | if(e==39) %(u)s(); |
---|
894 | else if(e==40) { |
---|
895 | if($('#%(key)s option:selected').next().length) |
---|
896 | $('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); |
---|
897 | %(u)s(); |
---|
898 | } else if(e==38) { |
---|
899 | if($('#%(key)s option:selected').prev().length) |
---|
900 | $('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); |
---|
901 | %(u)s(); |
---|
902 | } else if($('#%(id)s').val().length>=%(min_length)s) |
---|
903 | $.get('%(url)s&%(key)s='+encodeURIComponent($('#%(id)s').val()), |
---|
904 | function(data){ |
---|
905 | $('#%(id)s').next('.error').hide(); |
---|
906 | $('#%(div_id)s').html(data).show().focus(); |
---|
907 | $('#%(div_id)s select').css('width',$('#%(id)s').css('width')); |
---|
908 | $('#%(key)s').change(%(u)s).click(%(u)s); |
---|
909 | } |
---|
910 | ); |
---|
911 | else $('#%(div_id)s').fadeOut('slow'); |
---|
912 | } |
---|
913 | var tmr = null; |
---|
914 | $("#%(id)s").on('keyup focus',function(e) { |
---|
915 | if (tmr) clearTimeout(tmr); |
---|
916 | if($('#%(id)s').val().length>=%(min_length)s) { |
---|
917 | tmr = setTimeout(function() { tmr = null; doit(e); }, 300); |
---|
918 | } |
---|
919 | }); |
---|
920 | })(jQuery)""".replace('\n', '').replace(' ' * 4, '') % \ |
---|
921 | dict(url=self.url, min_length=self.min_length, |
---|
922 | key=self.keyword, id=attr['_id'], div_id=div_id, u='F' + self.keyword) |
---|
923 | return CAT(INPUT(**attr), SCRIPT(js), |
---|
924 | DIV(_id=div_id, _style='position:absolute;')) |
---|
925 | |
---|
926 | |
---|
927 | def formstyle_table3cols(form, fields): |
---|
928 | """ 3 column table - default """ |
---|
929 | table = TABLE() |
---|
930 | for id, label, controls, help in fields: |
---|
931 | _help = TD(help, _class='w2p_fc') |
---|
932 | _controls = TD(controls, _class='w2p_fw') |
---|
933 | _label = TD(label, _class='w2p_fl') |
---|
934 | table.append(TR(_label, _controls, _help, _id=id)) |
---|
935 | return table |
---|
936 | |
---|
937 | |
---|
938 | def formstyle_table2cols(form, fields): |
---|
939 | """ 2 column table """ |
---|
940 | table = TABLE() |
---|
941 | for id, label, controls, help in fields: |
---|
942 | _help = TD(help, _class='w2p_fc', _width='50%') |
---|
943 | _controls = TD(controls, _class='w2p_fw', _colspan='2') |
---|
944 | _label = TD(label, _class='w2p_fl', _width='50%') |
---|
945 | table.append(TR(_label, _help, _id=id + '1', _class='w2p_even even')) |
---|
946 | table.append(TR(_controls, _id=id + '2', _class='w2p_even odd')) |
---|
947 | return table |
---|
948 | |
---|
949 | |
---|
950 | def formstyle_divs(form, fields): |
---|
951 | """ divs only """ |
---|
952 | table = FIELDSET() |
---|
953 | for id, label, controls, help in fields: |
---|
954 | _help = DIV(help, _class='w2p_fc') |
---|
955 | _controls = DIV(controls, _class='w2p_fw') |
---|
956 | _label = DIV(label, _class='w2p_fl') |
---|
957 | table.append(DIV(_label, _controls, _help, _id=id)) |
---|
958 | return table |
---|
959 | |
---|
960 | |
---|
961 | def formstyle_inline(form, fields): |
---|
962 | """ divs only, but inline """ |
---|
963 | if len(fields) != 2: |
---|
964 | raise RuntimeError("Not possible") |
---|
965 | id, label, controls, help = fields[0] |
---|
966 | submit_button = fields[1][2] |
---|
967 | return CAT(DIV(controls, _style='display:inline'), |
---|
968 | submit_button) |
---|
969 | |
---|
970 | |
---|
971 | def formstyle_ul(form, fields): |
---|
972 | """ unordered list """ |
---|
973 | table = UL() |
---|
974 | for id, label, controls, help in fields: |
---|
975 | _help = DIV(help, _class='w2p_fc') |
---|
976 | _controls = DIV(controls, _class='w2p_fw') |
---|
977 | _label = DIV(label, _class='w2p_fl') |
---|
978 | table.append(LI(_label, _controls, _help, _id=id)) |
---|
979 | return table |
---|
980 | |
---|
981 | |
---|
982 | def formstyle_bootstrap(form, fields): |
---|
983 | """ bootstrap 2.3.x format form layout """ |
---|
984 | form.add_class('form-horizontal') |
---|
985 | parent = FIELDSET() |
---|
986 | for id, label, controls, help in fields: |
---|
987 | # wrappers |
---|
988 | _help = SPAN(help, _class='help-block') |
---|
989 | # embed _help into _controls |
---|
990 | _controls = DIV(controls, _help, _class='controls') |
---|
991 | # submit unflag by default |
---|
992 | _submit = False |
---|
993 | |
---|
994 | if isinstance(controls, INPUT): |
---|
995 | controls.add_class('span4') |
---|
996 | if controls['_type'] == 'submit': |
---|
997 | # flag submit button |
---|
998 | _submit = True |
---|
999 | controls['_class'] = 'btn btn-primary' |
---|
1000 | if controls['_type'] == 'file': |
---|
1001 | controls['_class'] = 'input-file' |
---|
1002 | |
---|
1003 | # For password fields, which are wrapped in a CAT object. |
---|
1004 | if isinstance(controls, CAT) and isinstance(controls[0], INPUT): |
---|
1005 | controls[0].add_class('span4') |
---|
1006 | |
---|
1007 | if isinstance(controls, SELECT): |
---|
1008 | controls.add_class('span4') |
---|
1009 | |
---|
1010 | if isinstance(controls, TEXTAREA): |
---|
1011 | controls.add_class('span4') |
---|
1012 | |
---|
1013 | if isinstance(label, LABEL): |
---|
1014 | label['_class'] = add_class(label.get('_class'), 'control-label') |
---|
1015 | |
---|
1016 | if _submit: |
---|
1017 | # submit button has unwrapped label and controls, different class |
---|
1018 | parent.append(DIV(label, controls, _class='form-actions', _id=id)) |
---|
1019 | # unflag submit (possible side effect) |
---|
1020 | _submit = False |
---|
1021 | else: |
---|
1022 | # unwrapped label |
---|
1023 | parent.append(DIV(label, _controls, _class='control-group', _id=id)) |
---|
1024 | return parent |
---|
1025 | |
---|
1026 | |
---|
1027 | def formstyle_bootstrap3_stacked(form, fields): |
---|
1028 | """ bootstrap 3 format form layout |
---|
1029 | |
---|
1030 | Note: |
---|
1031 | Experimental! |
---|
1032 | """ |
---|
1033 | parent = CAT() |
---|
1034 | for id, label, controls, help in fields: |
---|
1035 | # wrappers |
---|
1036 | _help = SPAN(help, _class='help-block') |
---|
1037 | # embed _help into _controls |
---|
1038 | _controls = CAT(controls, _help) |
---|
1039 | if isinstance(controls, INPUT): |
---|
1040 | if controls['_type'] == 'submit': |
---|
1041 | controls.add_class('btn btn-primary') |
---|
1042 | if controls['_type'] == 'button': |
---|
1043 | controls.add_class('btn btn-default') |
---|
1044 | elif controls['_type'] == 'file': |
---|
1045 | controls.add_class('input-file') |
---|
1046 | elif controls['_type'] in ('text', 'password'): |
---|
1047 | controls.add_class('form-control') |
---|
1048 | elif controls['_type'] == 'checkbox': |
---|
1049 | label['_for'] = None |
---|
1050 | label.insert(0, controls) |
---|
1051 | label.insert(0, ' ') |
---|
1052 | _controls = DIV(label, _help, _class="checkbox") |
---|
1053 | label = '' |
---|
1054 | elif isinstance(controls, (SELECT, TEXTAREA)): |
---|
1055 | controls.add_class('form-control') |
---|
1056 | |
---|
1057 | elif isinstance(controls, SPAN): |
---|
1058 | _controls = P(controls.components) |
---|
1059 | |
---|
1060 | elif isinstance(controls, UL): |
---|
1061 | for e in controls.elements("input"): |
---|
1062 | e.add_class('form-control') |
---|
1063 | |
---|
1064 | elif isinstance(controls, CAT) and isinstance(controls[0], INPUT): |
---|
1065 | controls[0].add_class('form-control') |
---|
1066 | |
---|
1067 | if isinstance(label, LABEL): |
---|
1068 | label['_class'] = add_class(label.get('_class'), 'control-label') |
---|
1069 | |
---|
1070 | parent.append(DIV(label, _controls, _class='form-group', _id=id)) |
---|
1071 | return parent |
---|
1072 | |
---|
1073 | |
---|
1074 | def formstyle_bootstrap3_inline_factory(col_label_size=3): |
---|
1075 | """ bootstrap 3 horizontal form layout |
---|
1076 | |
---|
1077 | Note: |
---|
1078 | Experimental! |
---|
1079 | """ |
---|
1080 | def _inner(form, fields): |
---|
1081 | form.add_class('form-horizontal') |
---|
1082 | label_col_class = "col-sm-%d" % col_label_size |
---|
1083 | col_class = "col-sm-%d" % (12 - col_label_size) |
---|
1084 | offset_class = "col-sm-offset-%d" % col_label_size |
---|
1085 | parent = CAT() |
---|
1086 | for id, label, controls, help in fields: |
---|
1087 | # wrappers |
---|
1088 | _help = SPAN(help, _class='help-block') |
---|
1089 | # embed _help into _controls |
---|
1090 | _controls = DIV(controls, _help, _class="%s" % (col_class)) |
---|
1091 | if isinstance(controls, INPUT): |
---|
1092 | if controls['_type'] == 'submit': |
---|
1093 | controls.add_class('btn btn-primary') |
---|
1094 | _controls = DIV(controls, _class="%s %s" % (col_class, offset_class)) |
---|
1095 | if controls['_type'] == 'button': |
---|
1096 | controls.add_class('btn btn-default') |
---|
1097 | elif controls['_type'] == 'file': |
---|
1098 | controls.add_class('input-file') |
---|
1099 | elif controls['_type'] in ('text', 'password'): |
---|
1100 | controls.add_class('form-control') |
---|
1101 | elif controls['_type'] == 'checkbox': |
---|
1102 | label['_for'] = None |
---|
1103 | label.insert(0, controls) |
---|
1104 | label.insert(1, ' ') |
---|
1105 | _controls = DIV(DIV(label, _help, _class="checkbox"), |
---|
1106 | _class="%s %s" % (offset_class, col_class)) |
---|
1107 | label = '' |
---|
1108 | elif isinstance(controls, (SELECT, TEXTAREA)): |
---|
1109 | controls.add_class('form-control') |
---|
1110 | |
---|
1111 | elif isinstance(controls, SPAN): |
---|
1112 | _controls = P(controls.components, |
---|
1113 | _class="form-control-static %s" % col_class) |
---|
1114 | elif isinstance(controls, UL): |
---|
1115 | for e in controls.elements("input"): |
---|
1116 | e.add_class('form-control') |
---|
1117 | elif isinstance(controls, CAT) and isinstance(controls[0], INPUT): |
---|
1118 | controls[0].add_class('form-control') |
---|
1119 | if isinstance(label, LABEL): |
---|
1120 | label['_class'] = add_class(label.get('_class'), 'control-label %s' % label_col_class) |
---|
1121 | |
---|
1122 | parent.append(DIV(label, _controls, _class='form-group', _id=id)) |
---|
1123 | return parent |
---|
1124 | return _inner |
---|
1125 | |
---|
1126 | # bootstrap 4 |
---|
1127 | def formstyle_bootstrap4_stacked(form, fields): |
---|
1128 | """ bootstrap 4 format form layout |
---|
1129 | |
---|
1130 | Note: |
---|
1131 | Experimental! |
---|
1132 | """ |
---|
1133 | parent = CAT() |
---|
1134 | for id, label, controls, help in fields: |
---|
1135 | # wrappers |
---|
1136 | _help = SPAN(help, _class='help-block') |
---|
1137 | # embed _help into _controls |
---|
1138 | _controls = CAT(controls, _help) |
---|
1139 | if isinstance(controls, INPUT): |
---|
1140 | if controls['_type'] == 'submit': |
---|
1141 | controls.add_class('btn btn-primary') |
---|
1142 | if controls['_type'] == 'button': |
---|
1143 | controls.add_class('btn btn-secondary') |
---|
1144 | elif controls['_type'] == 'file': |
---|
1145 | controls.add_class('form-control-file') |
---|
1146 | elif controls['_type'] in ('text', 'password'): |
---|
1147 | controls.add_class('form-control') |
---|
1148 | elif controls['_type'] == 'checkbox' or controls['_type'] == 'radio': |
---|
1149 | controls.add_class('form-check-input') |
---|
1150 | label['_for'] = None |
---|
1151 | label.add_class('form-check-label') |
---|
1152 | label.insert(0, controls) |
---|
1153 | label.insert(0, ' ') |
---|
1154 | _controls = DIV(label, _help, _class="form-check") |
---|
1155 | label = '' |
---|
1156 | elif isinstance(controls, (SELECT, TEXTAREA)): |
---|
1157 | controls.add_class('form-control') |
---|
1158 | |
---|
1159 | elif isinstance(controls, SPAN): |
---|
1160 | _controls = P(controls.components) |
---|
1161 | |
---|
1162 | elif isinstance(controls, UL): |
---|
1163 | for e in controls.elements("input"): |
---|
1164 | e.add_class('form-control') |
---|
1165 | |
---|
1166 | elif isinstance(controls, CAT) and isinstance(controls[0], INPUT): |
---|
1167 | controls[0].add_class('form-control') |
---|
1168 | |
---|
1169 | if isinstance(label, LABEL): |
---|
1170 | label['_class'] = add_class(label.get('_class'), 'form-control-label') |
---|
1171 | |
---|
1172 | parent.append(DIV(label, _controls, _class='form-group', _id=id)) |
---|
1173 | return parent |
---|
1174 | |
---|
1175 | |
---|
1176 | def formstyle_bootstrap4_inline_factory(col_label_size=3): |
---|
1177 | """ bootstrap 4 horizontal form layout |
---|
1178 | |
---|
1179 | Note: |
---|
1180 | Experimental! |
---|
1181 | """ |
---|
1182 | def _inner(form, fields): |
---|
1183 | label_col_class = "col-sm-%d" % col_label_size |
---|
1184 | col_class = "col-sm-%d" % (12 - col_label_size) |
---|
1185 | offset_class = "col-sm-offset-%d" % col_label_size |
---|
1186 | parent = CAT() |
---|
1187 | for id, label, controls, help in fields: |
---|
1188 | # wrappers |
---|
1189 | _help = SPAN(help, _class='help-block') |
---|
1190 | # embed _help into _controls |
---|
1191 | _controls = DIV(controls, _help, _class="%s" % (col_class)) |
---|
1192 | if isinstance(controls, INPUT): |
---|
1193 | if controls['_type'] == 'submit': |
---|
1194 | controls.add_class('btn btn-primary') |
---|
1195 | _controls = DIV(controls, _class="%s %s" % (col_class, offset_class)) |
---|
1196 | if controls['_type'] == 'button': |
---|
1197 | controls.add_class('btn btn-secondary') |
---|
1198 | elif controls['_type'] == 'file': |
---|
1199 | controls.add_class('input-file') |
---|
1200 | elif controls['_type'] in ('text', 'password'): |
---|
1201 | controls.add_class('form-control') |
---|
1202 | elif controls['_type'] == 'checkbox' or controls['_type'] == 'radio': |
---|
1203 | controls.add_class('form-check-input') |
---|
1204 | label.add_class('form-check-label') |
---|
1205 | label.insert(0, controls) |
---|
1206 | #label.insert(0, ' ') |
---|
1207 | _controls = DIV(DIV(label, _help, _class="form-check"), _class="%s" % col_class) |
---|
1208 | label = DIV(_class="sm-hidden %s" % label_col_class) |
---|
1209 | elif isinstance(controls, (SELECT, TEXTAREA)): |
---|
1210 | controls.add_class('form-control') |
---|
1211 | |
---|
1212 | elif isinstance(controls, SPAN): |
---|
1213 | _controls = P(controls.components, |
---|
1214 | _class="form-control-plaintext %s" % col_class) |
---|
1215 | elif isinstance(controls, UL): |
---|
1216 | for e in controls.elements("input"): |
---|
1217 | e.add_class('form-control') |
---|
1218 | elif isinstance(controls, CAT) and isinstance(controls[0], INPUT): |
---|
1219 | controls[0].add_class('form-control') |
---|
1220 | if isinstance(label, LABEL): |
---|
1221 | label['_class'] = add_class(label.get('_class'), 'form-control-label %s' % label_col_class) |
---|
1222 | |
---|
1223 | parent.append(DIV(label, _controls, _class='form-group row', _id=id)) |
---|
1224 | return parent |
---|
1225 | return _inner |
---|
1226 | |
---|
1227 | |
---|
1228 | class SQLFORM(FORM): |
---|
1229 | |
---|
1230 | """ |
---|
1231 | SQLFORM is used to map a table (and a current record) into an HTML form. |
---|
1232 | |
---|
1233 | Given a Table like db.table |
---|
1234 | |
---|
1235 | Generates an insert form:: |
---|
1236 | |
---|
1237 | SQLFORM(db.table) |
---|
1238 | |
---|
1239 | Generates an update form:: |
---|
1240 | |
---|
1241 | record=db.table[some_id] |
---|
1242 | SQLFORM(db.table, record) |
---|
1243 | |
---|
1244 | Generates an update with a delete button:: |
---|
1245 | |
---|
1246 | SQLFORM(db.table, record, deletable=True) |
---|
1247 | |
---|
1248 | Args: |
---|
1249 | table: `Table` object |
---|
1250 | record: either an int if the `id` is an int, or the record fetched |
---|
1251 | from the table |
---|
1252 | deletable: adds the delete checkbox |
---|
1253 | linkto: the URL of a controller/function to access referencedby |
---|
1254 | records |
---|
1255 | upload: the URL of a controller/function to download an uploaded file |
---|
1256 | fields: a list of fields that should be placed in the form, |
---|
1257 | default is all. |
---|
1258 | labels: a dictionary with labels for each field, keys are the field |
---|
1259 | names. |
---|
1260 | col3: a dictionary with content for an optional third column |
---|
1261 | (right of each field). keys are field names. |
---|
1262 | submit_button: text to show in the submit button |
---|
1263 | delete_label: text to show next to the delete checkbox |
---|
1264 | showid: shows the id of the record |
---|
1265 | readonly: doesn't allow for any modification |
---|
1266 | comments: show comments (stored in `col3` or in Field definition) |
---|
1267 | ignore_rw: overrides readable/writable attributes |
---|
1268 | record_id: used to create session key against CSRF |
---|
1269 | formstyle: what to use to generate the form layout |
---|
1270 | buttons: override buttons as you please (will be also stored in |
---|
1271 | `form.custom.submit`) |
---|
1272 | separator: character as separator between labels and inputs |
---|
1273 | |
---|
1274 | any named optional attribute is passed to the <form> tag |
---|
1275 | for example _class, _id, _style, _action, _method, etc. |
---|
1276 | |
---|
1277 | """ |
---|
1278 | |
---|
1279 | # usability improvements proposal by fpp - 4 May 2008 : |
---|
1280 | # - correct labels (for points to field id, not field name) |
---|
1281 | # - add label for delete checkbox |
---|
1282 | # - add translatable label for record ID |
---|
1283 | # - add third column to right of fields, populated from the col3 dict |
---|
1284 | |
---|
1285 | widgets = Storage( |
---|
1286 | string=StringWidget, |
---|
1287 | text=TextWidget, |
---|
1288 | json=JSONWidget, |
---|
1289 | password=PasswordWidget, |
---|
1290 | integer=IntegerWidget, |
---|
1291 | double=DoubleWidget, |
---|
1292 | decimal=DecimalWidget, |
---|
1293 | time=TimeWidget, |
---|
1294 | date=DateWidget, |
---|
1295 | datetime=DatetimeWidget, |
---|
1296 | upload=UploadWidget, |
---|
1297 | boolean=BooleanWidget, |
---|
1298 | blob=None, |
---|
1299 | options=OptionsWidget, |
---|
1300 | multiple=MultipleOptionsWidget, |
---|
1301 | radio=RadioWidget, |
---|
1302 | checkboxes=CheckboxesWidget, |
---|
1303 | autocomplete=AutocompleteWidget, |
---|
1304 | list=ListWidget, |
---|
1305 | ) |
---|
1306 | |
---|
1307 | formstyles = Storage( |
---|
1308 | table3cols=formstyle_table3cols, |
---|
1309 | table2cols=formstyle_table2cols, |
---|
1310 | divs=formstyle_divs, |
---|
1311 | ul=formstyle_ul, |
---|
1312 | bootstrap=formstyle_bootstrap, |
---|
1313 | bootstrap3_stacked=formstyle_bootstrap3_stacked, |
---|
1314 | bootstrap3_inline=formstyle_bootstrap3_inline_factory(3), |
---|
1315 | bootstrap4_stacked=formstyle_bootstrap4_stacked, |
---|
1316 | bootstrap4_inline=formstyle_bootstrap4_inline_factory(3), |
---|
1317 | inline=formstyle_inline, |
---|
1318 | ) |
---|
1319 | |
---|
1320 | FIELDNAME_REQUEST_DELETE = 'delete_this_record' |
---|
1321 | FIELDKEY_DELETE_RECORD = 'delete_record' |
---|
1322 | ID_LABEL_SUFFIX = '__label' |
---|
1323 | ID_ROW_SUFFIX = '__row' |
---|
1324 | |
---|
1325 | def assert_status(self, status, request_vars): |
---|
1326 | if not status and self.record and self.errors: |
---|
1327 | # if there are errors in update mode |
---|
1328 | # and some errors refers to an already uploaded file |
---|
1329 | # delete error if |
---|
1330 | # - user not trying to upload a new file |
---|
1331 | # - there is existing file and user is not trying to delete it |
---|
1332 | # this is because removing the file may not pass validation |
---|
1333 | for key in list(self.errors): |
---|
1334 | if key in self.table \ |
---|
1335 | and self.table[key].type == 'upload' \ |
---|
1336 | and request_vars.get(key, None) in (None, '') \ |
---|
1337 | and self.record[key] \ |
---|
1338 | and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars: |
---|
1339 | del self.errors[key] |
---|
1340 | if not self.errors: |
---|
1341 | status = True |
---|
1342 | return status |
---|
1343 | |
---|
1344 | def __init__( |
---|
1345 | self, |
---|
1346 | table, |
---|
1347 | record=None, |
---|
1348 | deletable=False, |
---|
1349 | linkto=None, |
---|
1350 | upload=None, |
---|
1351 | fields=None, |
---|
1352 | labels=None, |
---|
1353 | col3={}, |
---|
1354 | submit_button='Submit', |
---|
1355 | delete_label='Check to delete', |
---|
1356 | showid=True, |
---|
1357 | readonly=False, |
---|
1358 | comments=True, |
---|
1359 | keepopts=[], |
---|
1360 | ignore_rw=False, |
---|
1361 | record_id=None, |
---|
1362 | formstyle=None, |
---|
1363 | buttons=['submit'], |
---|
1364 | separator=None, |
---|
1365 | extra_fields=None, |
---|
1366 | **attributes |
---|
1367 | ): |
---|
1368 | T = current.T |
---|
1369 | |
---|
1370 | self.ignore_rw = ignore_rw |
---|
1371 | self.formstyle = formstyle or current.response.formstyle |
---|
1372 | self.readonly = readonly |
---|
1373 | # Default dbio setting |
---|
1374 | self.detect_record_change = None |
---|
1375 | |
---|
1376 | nbsp = XML(' ') # Firefox2 does not display fields with blanks |
---|
1377 | FORM.__init__(self, *[], **attributes) |
---|
1378 | ofields = fields |
---|
1379 | keyed = hasattr(table, '_primarykey') # for backward compatibility |
---|
1380 | |
---|
1381 | # if no fields are provided, build it from the provided table |
---|
1382 | # will only use writable or readable fields, unless forced to ignore |
---|
1383 | if fields is None: |
---|
1384 | if not readonly: |
---|
1385 | if not record: |
---|
1386 | # create form should only show writable fields |
---|
1387 | fields = [f.name for f in table if (ignore_rw or f.writable or (f.readable and f.default)) and not f.compute] |
---|
1388 | else: |
---|
1389 | # update form should also show readable fields and computed fields (but in reaodnly mode) |
---|
1390 | fields = [f.name for f in table if (ignore_rw or f.writable or f.readable)] |
---|
1391 | else: |
---|
1392 | # read only form should show all readable fields |
---|
1393 | fields = [f.name for f in table if (ignore_rw or f.readable)] |
---|
1394 | self.fields = fields |
---|
1395 | |
---|
1396 | # make sure we have an id |
---|
1397 | if self.fields[0] != table.fields[0] and \ |
---|
1398 | isinstance(table, Table) and not keyed: |
---|
1399 | self.fields.insert(0, table.fields[0]) |
---|
1400 | |
---|
1401 | self.table = table |
---|
1402 | |
---|
1403 | # try to retrieve the indicated record using its id |
---|
1404 | # otherwise ignore it |
---|
1405 | if record and isinstance(record, (int, long, str, unicodeT)): |
---|
1406 | if not str(record).isdigit(): |
---|
1407 | raise HTTP(404, "Object not found") |
---|
1408 | record = table._db(table._id == record).select().first() |
---|
1409 | if not record: |
---|
1410 | raise HTTP(404, "Object not found") |
---|
1411 | self.record = record |
---|
1412 | |
---|
1413 | if keyed: |
---|
1414 | self.record_id = dict([(k, record and str(record[k]) or None) |
---|
1415 | for k in table._primarykey]) |
---|
1416 | else: |
---|
1417 | self.record_id = record_id |
---|
1418 | |
---|
1419 | self.field_parent = {} |
---|
1420 | xfields = [] |
---|
1421 | self.custom = Storage() |
---|
1422 | self.custom.dspval = Storage() |
---|
1423 | self.custom.inpval = Storage() |
---|
1424 | self.custom.label = Storage() |
---|
1425 | self.custom.comment = Storage() |
---|
1426 | self.custom.widget = Storage() |
---|
1427 | self.custom.linkto = Storage() |
---|
1428 | |
---|
1429 | # default id field name |
---|
1430 | if not keyed: |
---|
1431 | self.id_field_name = table._id.name |
---|
1432 | else: |
---|
1433 | self.id_field_name = table._primarykey[0] # only works if one key |
---|
1434 | |
---|
1435 | sep = current.response.form_label_separator if separator is None else separator |
---|
1436 | |
---|
1437 | extra_fields = extra_fields or [] |
---|
1438 | self.extra_fields = {} |
---|
1439 | for extra_field in extra_fields: |
---|
1440 | if not extra_field.name in self.fields: |
---|
1441 | self.fields.append(extra_field.name) |
---|
1442 | self.extra_fields[extra_field.name] = extra_field |
---|
1443 | extra_field.db = table._db |
---|
1444 | extra_field.table = table |
---|
1445 | extra_field.tablename = table._tablename |
---|
1446 | if extra_field.requires == DEFAULT: |
---|
1447 | extra_field.requires = default_validators(table._db, extra_field) |
---|
1448 | |
---|
1449 | for fieldname in self.fields: |
---|
1450 | if fieldname.find('.') >= 0: |
---|
1451 | continue |
---|
1452 | |
---|
1453 | field = (self.table[fieldname] if fieldname in self.table.fields |
---|
1454 | else self.extra_fields[fieldname]) |
---|
1455 | comment = None |
---|
1456 | |
---|
1457 | if comments: |
---|
1458 | comment = col3.get(fieldname, field.comment) |
---|
1459 | if comment is None: |
---|
1460 | comment = '' |
---|
1461 | self.custom.comment[fieldname] = comment |
---|
1462 | |
---|
1463 | if labels is not None and fieldname in labels: |
---|
1464 | label = labels[fieldname] |
---|
1465 | else: |
---|
1466 | label = field.label |
---|
1467 | self.custom.label[fieldname] = label |
---|
1468 | |
---|
1469 | field_id = '%s_%s' % (table._tablename, fieldname) |
---|
1470 | |
---|
1471 | label = LABEL(label, label and sep, _for=field_id, |
---|
1472 | _id=field_id + SQLFORM.ID_LABEL_SUFFIX) |
---|
1473 | |
---|
1474 | cond = readonly or field.compute or \ |
---|
1475 | (not ignore_rw and not field.writable and field.readable) |
---|
1476 | |
---|
1477 | if cond: |
---|
1478 | label['_class'] = 'readonly' |
---|
1479 | else: |
---|
1480 | label['_class'] = '' |
---|
1481 | |
---|
1482 | row_id = field_id + SQLFORM.ID_ROW_SUFFIX |
---|
1483 | if field.type == 'id': |
---|
1484 | self.custom.dspval.id = nbsp |
---|
1485 | self.custom.inpval.id = '' |
---|
1486 | widget = '' |
---|
1487 | |
---|
1488 | # store the id field name (for legacy databases) |
---|
1489 | self.id_field_name = field.name |
---|
1490 | |
---|
1491 | if record: |
---|
1492 | if showid and field.name in record and field.readable: |
---|
1493 | v = record[field.name] |
---|
1494 | widget = SPAN(v, _id=field_id) |
---|
1495 | self.custom.dspval.id = str(v) |
---|
1496 | xfields.append((row_id, label, widget, comment)) |
---|
1497 | self.record_id = str(record[field.name]) |
---|
1498 | self.custom.widget.id = widget |
---|
1499 | continue |
---|
1500 | |
---|
1501 | if readonly and not ignore_rw and not field.readable: |
---|
1502 | continue |
---|
1503 | |
---|
1504 | if record: |
---|
1505 | default = record[fieldname] |
---|
1506 | else: |
---|
1507 | default = field.default |
---|
1508 | if isinstance(default, CALLABLETYPES): |
---|
1509 | default = default() |
---|
1510 | |
---|
1511 | if default is not None and not cond: |
---|
1512 | default = field.formatter(default) |
---|
1513 | |
---|
1514 | dspval = default |
---|
1515 | inpval = default |
---|
1516 | |
---|
1517 | if cond: |
---|
1518 | |
---|
1519 | if field.represent: |
---|
1520 | inp = represent(field, default, record) |
---|
1521 | elif field.type in ['blob']: |
---|
1522 | continue |
---|
1523 | elif field.type == 'upload': |
---|
1524 | inp = UploadWidget.represent(field, default, upload) |
---|
1525 | elif field.type == 'boolean': |
---|
1526 | inp = self.widgets.boolean.widget( |
---|
1527 | field, default, _disabled=True) |
---|
1528 | elif isinstance(field.type, SQLCustomType) and callable(field.type.represent): |
---|
1529 | # SQLCustomType has a represent, use it |
---|
1530 | inp = field.type.represent(default, record) |
---|
1531 | else: |
---|
1532 | inp = field.formatter(default) |
---|
1533 | if getattr(field, 'show_if', None): |
---|
1534 | if not isinstance(inp, XmlComponent): |
---|
1535 | # Create a container for string represents |
---|
1536 | inp = DIV(inp, _id='%s_%s' % (field.tablename, field.name)) |
---|
1537 | trigger, cond = show_if(field.show_if) |
---|
1538 | inp['_data-show-trigger'] = trigger |
---|
1539 | inp['_data-show-if'] = cond |
---|
1540 | elif field.type == 'upload': |
---|
1541 | if field.widget: |
---|
1542 | inp = field.widget(field, default, upload) |
---|
1543 | else: |
---|
1544 | inp = self.widgets.upload.widget(field, default, upload) |
---|
1545 | elif field.widget: |
---|
1546 | inp = field.widget(field, default) |
---|
1547 | elif field.type == 'boolean': |
---|
1548 | inp = self.widgets.boolean.widget(field, default) |
---|
1549 | if default: |
---|
1550 | inpval = 'checked' |
---|
1551 | else: |
---|
1552 | inpval = '' |
---|
1553 | elif OptionsWidget.has_options(field): |
---|
1554 | if not field.requires.multiple: |
---|
1555 | inp = self.widgets.options.widget(field, default) |
---|
1556 | else: |
---|
1557 | inp = self.widgets.multiple.widget(field, default) |
---|
1558 | if fieldname in keepopts: |
---|
1559 | inpval = CAT(*inp.components) |
---|
1560 | elif field.type.startswith('list:'): |
---|
1561 | inp = self.widgets.list.widget(field, default) |
---|
1562 | elif field.type == 'text': |
---|
1563 | inp = self.widgets.text.widget(field, default) |
---|
1564 | elif field.type == 'password': |
---|
1565 | inp = self.widgets.password.widget(field, default) |
---|
1566 | if self.record: |
---|
1567 | dspval = DEFAULT_PASSWORD_DISPLAY |
---|
1568 | else: |
---|
1569 | dspval = '' |
---|
1570 | elif field.type == 'blob': |
---|
1571 | continue |
---|
1572 | elif isinstance(field.type, SQLCustomType) and callable(field.type.widget): |
---|
1573 | # SQLCustomType has a widget, use it |
---|
1574 | inp = field.type.widget(field, default) |
---|
1575 | else: |
---|
1576 | field_type = REGEX_WIDGET_CLASS.match(str(field.type)).group() |
---|
1577 | field_type = field_type in self.widgets and field_type or 'string' |
---|
1578 | inp = self.widgets[field_type].widget(field, default) |
---|
1579 | |
---|
1580 | xfields.append((row_id, label, inp, comment)) |
---|
1581 | self.custom.dspval[fieldname] = dspval if (dspval is not None) else nbsp |
---|
1582 | self.custom.inpval[fieldname] = inpval if inpval is not None else '' |
---|
1583 | self.custom.widget[fieldname] = inp |
---|
1584 | |
---|
1585 | # if a record is provided and found, as is linkto |
---|
1586 | # build a link |
---|
1587 | if record and linkto: |
---|
1588 | db = linkto.split('/')[-1] |
---|
1589 | for rfld in table._referenced_by: |
---|
1590 | if keyed: |
---|
1591 | query = urllib_quote('%s.%s==%s' % ( |
---|
1592 | db, rfld, record[rfld.type[10:].split('.')[1]])) |
---|
1593 | else: |
---|
1594 | query = urllib_quote( |
---|
1595 | '%s.%s==%s' % (db, rfld, record[self.id_field_name])) |
---|
1596 | lname = olname = '%s.%s' % (rfld.tablename, rfld.name) |
---|
1597 | if ofields and olname not in ofields: |
---|
1598 | continue |
---|
1599 | if labels and lname in labels: |
---|
1600 | lname = labels[lname] |
---|
1601 | widget = A(lname, |
---|
1602 | _class='reference', |
---|
1603 | _href='%s/%s?query=%s' % (linkto, rfld.tablename, query)) |
---|
1604 | xfields.append( |
---|
1605 | (olname.replace('.', '__') + SQLFORM.ID_ROW_SUFFIX, |
---|
1606 | '', widget, col3.get(olname, ''))) |
---|
1607 | self.custom.linkto[olname.replace('.', '__')] = widget |
---|
1608 | # </block> |
---|
1609 | |
---|
1610 | # when deletable, add delete? checkbox |
---|
1611 | self.custom.delete = self.custom.deletable = '' |
---|
1612 | if record and deletable: |
---|
1613 | # add secondary css class for cascade delete warning |
---|
1614 | css = 'delete' |
---|
1615 | for f in self.table.fields: |
---|
1616 | on_del = self.table[f].ondelete |
---|
1617 | if isinstance(on_del, str) and 'cascade' in on_del.lower(): |
---|
1618 | css += ' cascade_delete' |
---|
1619 | break |
---|
1620 | widget = INPUT(_type='checkbox', |
---|
1621 | _class=css, |
---|
1622 | _id=self.FIELDKEY_DELETE_RECORD, |
---|
1623 | _name=self.FIELDNAME_REQUEST_DELETE, |
---|
1624 | ) |
---|
1625 | xfields.append( |
---|
1626 | (self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_ROW_SUFFIX, |
---|
1627 | LABEL( |
---|
1628 | T(delete_label), sep, |
---|
1629 | _for=self.FIELDKEY_DELETE_RECORD, |
---|
1630 | _id=self.FIELDKEY_DELETE_RECORD + |
---|
1631 | SQLFORM.ID_LABEL_SUFFIX), |
---|
1632 | widget, |
---|
1633 | col3.get(self.FIELDKEY_DELETE_RECORD, ''))) |
---|
1634 | self.custom.delete = self.custom.deletable = widget |
---|
1635 | |
---|
1636 | # when writable, add submit button |
---|
1637 | self.custom.submit = '' |
---|
1638 | if not readonly: |
---|
1639 | if 'submit' in buttons: |
---|
1640 | widget = self.custom.submit = INPUT(_type='submit', |
---|
1641 | _value=T(submit_button)) |
---|
1642 | elif buttons: |
---|
1643 | widget = self.custom.submit = DIV(*buttons) |
---|
1644 | if self.custom.submit: |
---|
1645 | xfields.append(('submit_record' + SQLFORM.ID_ROW_SUFFIX, |
---|
1646 | '', widget, col3.get('submit_button', ''))) |
---|
1647 | |
---|
1648 | # if a record is provided and found |
---|
1649 | # make sure it's id is stored in the form |
---|
1650 | if record: |
---|
1651 | if not self['hidden']: |
---|
1652 | self['hidden'] = {} |
---|
1653 | if not keyed: |
---|
1654 | self['hidden']['id'] = record[table._id.name] |
---|
1655 | |
---|
1656 | (begin, end) = self._xml() |
---|
1657 | self.custom.begin = XML("<%s %s>" % (self.tag, to_native(begin))) |
---|
1658 | self.custom.end = XML("%s</%s>" % (to_native(end), self.tag)) |
---|
1659 | table = self.createform(xfields) |
---|
1660 | self.components = [table] |
---|
1661 | |
---|
1662 | def createform(self, xfields): |
---|
1663 | formstyle = self.formstyle |
---|
1664 | if isinstance(formstyle, basestring): |
---|
1665 | if formstyle in SQLFORM.formstyles: |
---|
1666 | formstyle = SQLFORM.formstyles[formstyle] |
---|
1667 | else: |
---|
1668 | raise RuntimeError('formstyle not found') |
---|
1669 | |
---|
1670 | if callable(formstyle): |
---|
1671 | try: |
---|
1672 | table = formstyle(self, xfields) |
---|
1673 | for id, a, b, c in xfields: |
---|
1674 | self.field_parent[id] = getattr(b, 'parent', None) \ |
---|
1675 | if isinstance(b, XmlComponent) else None |
---|
1676 | except TypeError: |
---|
1677 | # backward compatibility, 4 argument function is the old style |
---|
1678 | table = TABLE() |
---|
1679 | for id, a, b, c in xfields: |
---|
1680 | newrows = formstyle(id, a, b, c) |
---|
1681 | self.field_parent[id] = getattr(b, 'parent', None) \ |
---|
1682 | if isinstance(b, XmlComponent) else None |
---|
1683 | if type(newrows).__name__ != "tuple": |
---|
1684 | newrows = [newrows] |
---|
1685 | for newrow in newrows: |
---|
1686 | table.append(newrow) |
---|
1687 | else: |
---|
1688 | raise RuntimeError('formstyle not supported') |
---|
1689 | return table |
---|
1690 | |
---|
1691 | def accepts( |
---|
1692 | self, |
---|
1693 | request_vars, |
---|
1694 | session=None, |
---|
1695 | formname='%(tablename)s/%(record_id)s', |
---|
1696 | keepvalues=None, |
---|
1697 | onvalidation=None, |
---|
1698 | dbio=True, |
---|
1699 | hideerror=False, |
---|
1700 | detect_record_change=False, |
---|
1701 | **kwargs |
---|
1702 | ): |
---|
1703 | |
---|
1704 | """ |
---|
1705 | Similar to `FORM.accepts` but also does insert, update or delete in DAL. |
---|
1706 | If detect_record_change is `True` then: |
---|
1707 | |
---|
1708 | - `form.record_changed = False` (record is properly validated/submitted) |
---|
1709 | - `form.record_changed = True` (record cannot be submitted because changed) |
---|
1710 | |
---|
1711 | If detect_record_change == False then: |
---|
1712 | |
---|
1713 | - `form.record_changed = None` |
---|
1714 | """ |
---|
1715 | |
---|
1716 | if keepvalues is None: |
---|
1717 | keepvalues = True if self.record else False |
---|
1718 | |
---|
1719 | if self.readonly: |
---|
1720 | self.deleted = False |
---|
1721 | return False |
---|
1722 | |
---|
1723 | if request_vars.__class__.__name__ == 'Request': |
---|
1724 | request_vars = request_vars.post_vars |
---|
1725 | |
---|
1726 | keyed = hasattr(self.table, '_primarykey') |
---|
1727 | |
---|
1728 | # implement logic to detect whether record exist but has been modified |
---|
1729 | # server side |
---|
1730 | self.record_changed = None |
---|
1731 | self.detect_record_change = detect_record_change |
---|
1732 | if self.detect_record_change: |
---|
1733 | if self.record: |
---|
1734 | self.record_changed = False |
---|
1735 | serialized = '|'.join( |
---|
1736 | str(self.record[k]) for k in self.table.fields()) |
---|
1737 | self.record_hash = md5_hash(serialized) |
---|
1738 | |
---|
1739 | # logic to deal with record_id for keyed tables |
---|
1740 | if self.record: |
---|
1741 | if keyed: |
---|
1742 | formname_id = '.'.join(str(self.record[k]) |
---|
1743 | for k in self.table._primarykey |
---|
1744 | if hasattr(self.record, k)) |
---|
1745 | record_id = dict((k, request_vars.get(k, None)) |
---|
1746 | for k in self.table._primarykey) |
---|
1747 | else: |
---|
1748 | (formname_id, record_id) = (self.record[self.id_field_name], |
---|
1749 | request_vars.get('id', None)) |
---|
1750 | keepvalues = True |
---|
1751 | else: |
---|
1752 | if keyed: |
---|
1753 | formname_id = 'create' |
---|
1754 | record_id = dict([(k, None) for k in self.table._primarykey]) |
---|
1755 | else: |
---|
1756 | (formname_id, record_id) = ('create', None) |
---|
1757 | |
---|
1758 | if not keyed and isinstance(record_id, (list, tuple)): |
---|
1759 | record_id = record_id[0] |
---|
1760 | |
---|
1761 | if formname: |
---|
1762 | formname = formname % dict(tablename=self.table._tablename, |
---|
1763 | record_id=formname_id) |
---|
1764 | |
---|
1765 | # ## THIS IS FOR UNIQUE RECORDS, read IS_NOT_IN_DB |
---|
1766 | |
---|
1767 | for fieldname in self.fields: |
---|
1768 | field = (self.table[fieldname] if fieldname in self.table.fields |
---|
1769 | else self.extra_fields[fieldname]) |
---|
1770 | requires = field.requires or [] |
---|
1771 | if not isinstance(requires, (list, tuple)): |
---|
1772 | requires = [requires] |
---|
1773 | [item.set_self_id(self.record_id) for item in requires |
---|
1774 | if hasattr(item, 'set_self_id') and self.record_id] |
---|
1775 | |
---|
1776 | # ## END |
---|
1777 | |
---|
1778 | fields = {} |
---|
1779 | for key in self.vars: |
---|
1780 | fields[key] = self.vars[key] |
---|
1781 | |
---|
1782 | ret = FORM.accepts( |
---|
1783 | self, |
---|
1784 | request_vars, |
---|
1785 | session, |
---|
1786 | formname, |
---|
1787 | keepvalues, |
---|
1788 | onvalidation, |
---|
1789 | hideerror=hideerror, |
---|
1790 | **kwargs |
---|
1791 | ) |
---|
1792 | |
---|
1793 | self.deleted = request_vars.get(self.FIELDNAME_REQUEST_DELETE, False) |
---|
1794 | |
---|
1795 | self.custom.end = CAT(self.hidden_fields(), self.custom.end) |
---|
1796 | |
---|
1797 | delete_exception = self.record_id and self.errors and self.deleted |
---|
1798 | |
---|
1799 | if self.record_changed and self.detect_record_change: |
---|
1800 | message_onchange = \ |
---|
1801 | kwargs.setdefault("message_onchange", |
---|
1802 | current.T("A record change was detected. " + |
---|
1803 | "Consecutive update self-submissions " + |
---|
1804 | "are not allowed. Try re-submitting or " + |
---|
1805 | "refreshing the form page.")) |
---|
1806 | if message_onchange is not None: |
---|
1807 | current.response.flash = message_onchange |
---|
1808 | return ret |
---|
1809 | |
---|
1810 | elif (not ret) and (not delete_exception): |
---|
1811 | # delete_exception is true when user tries to delete a record |
---|
1812 | # that does not pass validation, yet it should be deleted |
---|
1813 | for fieldname in self.fields: |
---|
1814 | |
---|
1815 | field = (self.table[fieldname] |
---|
1816 | if fieldname in self.table.fields |
---|
1817 | else self.extra_fields[fieldname]) |
---|
1818 | # this is a workaround! widgets should always have default not None! |
---|
1819 | if not field.widget and field.type.startswith('list:') and \ |
---|
1820 | not OptionsWidget.has_options(field): |
---|
1821 | field.widget = self.widgets.list.widget |
---|
1822 | if field.widget == self.widgets.list.widget and fieldname in request_vars: |
---|
1823 | if fieldname in self.request_vars: |
---|
1824 | value = self.request_vars[fieldname] |
---|
1825 | elif self.record: |
---|
1826 | value = self.record[fieldname] |
---|
1827 | else: |
---|
1828 | value = field.default |
---|
1829 | row_id = '%s_%s%s' % ( |
---|
1830 | self.table, fieldname, SQLFORM.ID_ROW_SUFFIX) |
---|
1831 | widget = field.widget(field, value) |
---|
1832 | parent = self.field_parent[row_id] |
---|
1833 | if parent: |
---|
1834 | parent.components = [widget] |
---|
1835 | if self.errors.get(fieldname): |
---|
1836 | parent._traverse(False, hideerror) |
---|
1837 | self.custom.widget[fieldname] = widget |
---|
1838 | self.accepted = ret |
---|
1839 | return ret |
---|
1840 | |
---|
1841 | if self.record_id: |
---|
1842 | if str(record_id) != str(self.record_id): |
---|
1843 | raise SyntaxError('user is tampering with form\'s record_id: ' |
---|
1844 | '%s != %s' % (record_id, self.record_id)) |
---|
1845 | |
---|
1846 | if record_id and dbio and not keyed: |
---|
1847 | self.vars.id = self.record[self.id_field_name] |
---|
1848 | |
---|
1849 | if self.deleted and self.custom.deletable: |
---|
1850 | if dbio: |
---|
1851 | if keyed: |
---|
1852 | qry = reduce(lambda x, y: x & y, |
---|
1853 | [self.table[k] == record_id[k] |
---|
1854 | for k in self.table._primarykey]) |
---|
1855 | else: |
---|
1856 | qry = self.table._id == self.record[self.id_field_name] |
---|
1857 | self.table._db(qry).delete() |
---|
1858 | self.errors.clear() |
---|
1859 | for component in self.elements('input, select, textarea'): |
---|
1860 | component['_disabled'] = True |
---|
1861 | self.accepted = True |
---|
1862 | return True |
---|
1863 | |
---|
1864 | for fieldname in self.fields: |
---|
1865 | if fieldname not in self.table.fields: |
---|
1866 | continue |
---|
1867 | |
---|
1868 | if not self.ignore_rw and not self.table[fieldname].writable: |
---|
1869 | # this happens because FORM has no knowledge of writable |
---|
1870 | # and thinks that a missing boolean field is a None |
---|
1871 | if self.table[fieldname].type == 'boolean' and \ |
---|
1872 | self.vars.get(fieldname, True) is None: |
---|
1873 | del self.vars[fieldname] |
---|
1874 | continue |
---|
1875 | |
---|
1876 | field = self.table[fieldname] |
---|
1877 | if field.type == 'id' or field.compute: |
---|
1878 | continue |
---|
1879 | if field.type == 'boolean': |
---|
1880 | if self.vars.get(fieldname, False): |
---|
1881 | self.vars[fieldname] = fields[fieldname] = True |
---|
1882 | else: |
---|
1883 | self.vars[fieldname] = fields[fieldname] = False |
---|
1884 | elif field.type == 'password' and self.record\ |
---|
1885 | and request_vars.get(fieldname, None) == DEFAULT_PASSWORD_DISPLAY: |
---|
1886 | continue # do not update if password was not changed |
---|
1887 | elif field.type == 'upload': |
---|
1888 | f = self.vars[fieldname] |
---|
1889 | fd = '%s__delete' % fieldname |
---|
1890 | if f == '' or f is None: |
---|
1891 | if self.vars.get(fd, False): |
---|
1892 | f = self.table[fieldname].default or '' |
---|
1893 | fields[fieldname] = f |
---|
1894 | elif self.record: |
---|
1895 | if self.record[fieldname]: |
---|
1896 | fields[fieldname] = self.record[fieldname] |
---|
1897 | else: |
---|
1898 | f = self.table[fieldname].default or '' |
---|
1899 | fields[fieldname] = f |
---|
1900 | else: |
---|
1901 | f = self.table[fieldname].default or '' |
---|
1902 | fields[fieldname] = f |
---|
1903 | self.vars[fieldname] = fields[fieldname] |
---|
1904 | if not f: |
---|
1905 | continue |
---|
1906 | else: |
---|
1907 | f = os.path.join( |
---|
1908 | current.request.folder, |
---|
1909 | os.path.normpath(f)) |
---|
1910 | source_file = open(f, 'rb') |
---|
1911 | original_filename = os.path.split(f)[1] |
---|
1912 | elif hasattr(f, 'file'): |
---|
1913 | (source_file, original_filename) = (f.file, f.filename) |
---|
1914 | elif isinstance(f, (str, unicodeT)): |
---|
1915 | # do not know why this happens, it should not |
---|
1916 | (source_file, original_filename) = \ |
---|
1917 | (StringIO(f), 'file.txt') |
---|
1918 | else: |
---|
1919 | # this should never happen, why does it happen? |
---|
1920 | # print 'f=', repr(f) |
---|
1921 | continue |
---|
1922 | newfilename = field.store(source_file, original_filename, |
---|
1923 | field.uploadfolder) |
---|
1924 | # this line was for backward compatibility but problematic |
---|
1925 | # self.vars['%s_newfilename' % fieldname] = newfilename |
---|
1926 | fields[fieldname] = newfilename |
---|
1927 | if isinstance(field.uploadfield, str): |
---|
1928 | fields[field.uploadfield] = source_file.read() |
---|
1929 | # proposed by Hamdy (accept?) do we need fields at this point? |
---|
1930 | self.vars[fieldname] = fields[fieldname] |
---|
1931 | continue |
---|
1932 | elif fieldname in self.vars: |
---|
1933 | fields[fieldname] = self.vars[fieldname] |
---|
1934 | elif field.default is None and field.type != 'blob': |
---|
1935 | self.errors[fieldname] = 'no data' |
---|
1936 | self.accepted = False |
---|
1937 | return False |
---|
1938 | value = fields.get(fieldname, None) |
---|
1939 | if field.type == 'list:string': |
---|
1940 | if not isinstance(value, (tuple, list)): |
---|
1941 | fields[fieldname] = value and [value] or [] |
---|
1942 | elif isinstance(field.type, str) and field.type.startswith('list:'): |
---|
1943 | if not isinstance(value, list): |
---|
1944 | fields[fieldname] = [safe_int( |
---|
1945 | x) for x in (value and [value] or [])] |
---|
1946 | elif field.type == 'integer': |
---|
1947 | fields[fieldname] = safe_int(value, None) |
---|
1948 | elif field.type.startswith('reference'): |
---|
1949 | if isinstance(self.table, Table) and not keyed: |
---|
1950 | fields[fieldname] = safe_int(value, None) |
---|
1951 | elif field.type == 'double': |
---|
1952 | if value is not None: |
---|
1953 | fields[fieldname] = safe_float(value) |
---|
1954 | |
---|
1955 | for fieldname in self.vars: |
---|
1956 | if fieldname != 'id' and fieldname in self.table.fields\ |
---|
1957 | and fieldname not in fields and not fieldname\ |
---|
1958 | in request_vars: |
---|
1959 | fields[fieldname] = self.vars[fieldname] |
---|
1960 | |
---|
1961 | if dbio: |
---|
1962 | for fieldname in self.extra_fields: |
---|
1963 | if fieldname in fields: |
---|
1964 | del fields[fieldname] |
---|
1965 | if 'delete_this_record' in fields: |
---|
1966 | # this should never happen but seems to happen to some |
---|
1967 | del fields['delete_this_record'] |
---|
1968 | for field in self.table: |
---|
1969 | if field.name not in fields and field.writable is False \ |
---|
1970 | and field.update is None and field.compute is None: |
---|
1971 | if record_id and self.record: |
---|
1972 | fields[field.name] = self.record[field.name] |
---|
1973 | elif not self.table[field.name].default is None: |
---|
1974 | fields[field.name] = self.table[field.name].default |
---|
1975 | if keyed: |
---|
1976 | if reduce(lambda x, y: x and y, record_id.values()): # if record_id |
---|
1977 | if fields: |
---|
1978 | qry = reduce(lambda x, y: x & y, |
---|
1979 | [self.table[k] == self.record[k] for k in self.table._primarykey]) |
---|
1980 | self.table._db(qry).update(**fields) |
---|
1981 | else: |
---|
1982 | pk = self.table.insert(**fields) |
---|
1983 | if pk: |
---|
1984 | self.vars.update(pk) |
---|
1985 | else: |
---|
1986 | ret = False |
---|
1987 | elif self.table._db._uri: |
---|
1988 | if record_id: |
---|
1989 | self.vars.id = self.record[self.id_field_name] |
---|
1990 | if fields: |
---|
1991 | self.table._db(self.table._id == self.record[ |
---|
1992 | self.id_field_name]).update(**fields) |
---|
1993 | else: |
---|
1994 | self.vars.id = self.table.insert(**fields) |
---|
1995 | |
---|
1996 | self.accepted = ret |
---|
1997 | return ret |
---|
1998 | |
---|
1999 | AUTOTYPES = { |
---|
2000 | type(''): ('string', None), |
---|
2001 | type(''): ('string',None), |
---|
2002 | type(True): ('boolean', None), |
---|
2003 | type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)), |
---|
2004 | type(1.0): ('double', IS_FLOAT_IN_RANGE()), |
---|
2005 | type([]): ('list:string', None), |
---|
2006 | type(datetime.date.today()): ('date', IS_DATE()), |
---|
2007 | type(datetime.datetime.today()): ('datetime', IS_DATETIME()) |
---|
2008 | } |
---|
2009 | |
---|
2010 | @staticmethod |
---|
2011 | def dictform(dictionary, **kwargs): |
---|
2012 | fields = [] |
---|
2013 | for key, value in sorted(dictionary.items()): |
---|
2014 | t, requires = SQLFORM.AUTOTYPES.get(type(value), (None, None)) |
---|
2015 | if t: |
---|
2016 | fields.append(Field(key, t, requires=requires, |
---|
2017 | default=value)) |
---|
2018 | return SQLFORM.factory(*fields, **kwargs) |
---|
2019 | |
---|
2020 | @staticmethod |
---|
2021 | def smartdictform(session, name, filename=None, query=None, **kwargs): |
---|
2022 | if query: |
---|
2023 | session[name] = query.db(query).select().first().as_dict() |
---|
2024 | elif os.path.exists(filename): |
---|
2025 | env = {'datetime': datetime} |
---|
2026 | session[name] = eval(open(filename).read(), {}, env) |
---|
2027 | form = SQLFORM.dictform(session[name]) |
---|
2028 | if form.process().accepted: |
---|
2029 | session[name].update(form.vars) |
---|
2030 | if query: |
---|
2031 | query.db(query).update(**form.vars) |
---|
2032 | else: |
---|
2033 | open(filename, 'w').write(repr(session[name])) |
---|
2034 | return form |
---|
2035 | |
---|
2036 | @staticmethod |
---|
2037 | def factory(*fields, **attributes): |
---|
2038 | """ |
---|
2039 | Generates a SQLFORM for the given fields. |
---|
2040 | |
---|
2041 | Internally will build a non-database based data model |
---|
2042 | to hold the fields. |
---|
2043 | """ |
---|
2044 | # this is here to avoid circular references |
---|
2045 | from gluon.dal import DAL |
---|
2046 | # Define a table name, this way it can be logical to our CSS. |
---|
2047 | # And if you switch from using SQLFORM to SQLFORM.factory |
---|
2048 | # your same css definitions will still apply. |
---|
2049 | |
---|
2050 | table_name = attributes.get('table_name', 'no_table') |
---|
2051 | |
---|
2052 | # So it won't interfere with DAL.define_table |
---|
2053 | if 'table_name' in attributes: |
---|
2054 | del attributes['table_name'] |
---|
2055 | |
---|
2056 | # Clone fields, while passing tables straight through |
---|
2057 | fields_with_clones = [f.clone() if isinstance(f, Field) else f for f in fields] |
---|
2058 | dummy_dal = DAL(None) |
---|
2059 | dummy_dal.validators_method = lambda f: default_validators(dummy_dal, f) # See https://github.com/web2py/web2py/issues/2007 |
---|
2060 | return SQLFORM(dummy_dal.define_table(table_name, *fields_with_clones), **attributes) |
---|
2061 | |
---|
2062 | @staticmethod |
---|
2063 | def build_query(fields, keywords): |
---|
2064 | request = current.request |
---|
2065 | if isinstance(keywords, (tuple, list)): |
---|
2066 | keywords = keywords[0] |
---|
2067 | request.vars.keywords = keywords |
---|
2068 | key = keywords.strip() |
---|
2069 | if key and not '"' in key: |
---|
2070 | SEARCHABLE_TYPES = ('string', 'text', 'list:string') |
---|
2071 | sfields = [field for field in fields if field.type in SEARCHABLE_TYPES] |
---|
2072 | if settings.global_settings.web2py_runtime_gae: |
---|
2073 | return reduce(lambda a,b: a|b, [field.contains(key) for field in sfields]) |
---|
2074 | else: |
---|
2075 | if not (sfields and key and key.split()): |
---|
2076 | return fields[0].table |
---|
2077 | return reduce(lambda a,b:a&b,[ |
---|
2078 | reduce(lambda a,b: a|b, [ |
---|
2079 | field.contains(k) for field in sfields] |
---|
2080 | ) for k in key.split()]) |
---|
2081 | |
---|
2082 | # from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4 |
---|
2083 | # needs testing... |
---|
2084 | #words = key.split(' ') if key else [] |
---|
2085 | #filters = [] |
---|
2086 | #for field in fields: |
---|
2087 | # if field.type in SEARCHABLE_TYPES: |
---|
2088 | # all_words_filters = [] |
---|
2089 | # for word in words: |
---|
2090 | # all_words_filters.append(field.contains(word)) |
---|
2091 | # filters.append(reduce(lambda a, b: (a & b), all_words_filters)) |
---|
2092 | #parts = filters |
---|
2093 | |
---|
2094 | else: |
---|
2095 | return smart_query(fields, key) |
---|
2096 | |
---|
2097 | @staticmethod |
---|
2098 | def search_menu(fields, |
---|
2099 | search_options=None, |
---|
2100 | prefix='w2p' |
---|
2101 | ): |
---|
2102 | T = current.T |
---|
2103 | panel_id = '%s_query_panel' % prefix |
---|
2104 | fields_id = '%s_query_fields' % prefix |
---|
2105 | keywords_id = '%s_keywords' % prefix |
---|
2106 | field_id = '%s_field' % prefix |
---|
2107 | value_id = '%s_value' % prefix |
---|
2108 | search_options = search_options or { |
---|
2109 | 'string': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'], |
---|
2110 | 'text': ['=', '!=', '<', '>', '<=', '>=', 'starts with', 'contains', 'in', 'not in'], |
---|
2111 | 'date': ['=', '!=', '<', '>', '<=', '>='], |
---|
2112 | 'time': ['=', '!=', '<', '>', '<=', '>='], |
---|
2113 | 'datetime': ['=', '!=', '<', '>', '<=', '>='], |
---|
2114 | 'integer': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], |
---|
2115 | 'double': ['=', '!=', '<', '>', '<=', '>='], |
---|
2116 | 'id': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], |
---|
2117 | 'reference': ['=', '!='], |
---|
2118 | 'boolean': ['=', '!=']} |
---|
2119 | if fields[0]._db._adapter.dbengine == 'google:datastore': |
---|
2120 | search_options['string'] = ['=', '!=', '<', '>', '<=', '>='] |
---|
2121 | search_options['text'] = ['=', '!=', '<', '>', '<=', '>='] |
---|
2122 | search_options['list:string'] = ['contains'] |
---|
2123 | search_options['list:integer'] = ['contains'] |
---|
2124 | search_options['list:reference'] = ['contains'] |
---|
2125 | criteria = [] |
---|
2126 | selectfields = [] |
---|
2127 | for field in fields: |
---|
2128 | name = str(field).replace('.', '-') |
---|
2129 | # treat ftype 'decimal' as 'double' |
---|
2130 | # (this fixes problems but needs refactoring! |
---|
2131 | if isinstance(field.type, SQLCustomType): |
---|
2132 | ftype = field.type.type.split(' ')[0] |
---|
2133 | else: |
---|
2134 | ftype = field.type.split(' ')[0] |
---|
2135 | if ftype.startswith('decimal'): |
---|
2136 | ftype = 'double' |
---|
2137 | elif ftype == 'bigint': |
---|
2138 | ftype = 'integer' |
---|
2139 | elif ftype.startswith('big-'): |
---|
2140 | ftype = ftype[4:] |
---|
2141 | # end |
---|
2142 | options = search_options.get(ftype, None) |
---|
2143 | if options: |
---|
2144 | label = isinstance( |
---|
2145 | field.label, str) and T(field.label) or field.label |
---|
2146 | selectfields.append(OPTION(label, _value=str(field))) |
---|
2147 | # At web2py level SQLCustomType field values are treated as normal web2py types |
---|
2148 | if isinstance(field.type, SQLCustomType): |
---|
2149 | field_type = field.type.type |
---|
2150 | else: |
---|
2151 | field_type = field.type |
---|
2152 | |
---|
2153 | operators = SELECT(*[OPTION(T(option), _value=option) for option in options], |
---|
2154 | _class='form-control') |
---|
2155 | _id = "%s_%s" % (value_id, name) |
---|
2156 | if field_type in ['boolean', 'double', 'time', 'integer']: |
---|
2157 | widget_ = SQLFORM.widgets[field_type] |
---|
2158 | value_input = widget_.widget(field, field.default, _id=_id, |
---|
2159 | _class=widget_._class + ' form-control') |
---|
2160 | elif field_type == 'date': |
---|
2161 | iso_format = {'_data-w2p_date_format': '%Y-%m-%d'} |
---|
2162 | widget_ = SQLFORM.widgets.date |
---|
2163 | value_input = widget_.widget(field, field.default, _id=_id, |
---|
2164 | _class=widget_._class + ' form-control', |
---|
2165 | **iso_format) |
---|
2166 | elif field_type == 'datetime': |
---|
2167 | iso_format = {'_data-w2p_datetime_format': '%Y-%m-%d %H:%M:%S'} |
---|
2168 | widget_ = SQLFORM.widgets.datetime |
---|
2169 | value_input = widget_.widget(field, field.default, _id=_id, |
---|
2170 | _class=widget_._class + ' form-control', |
---|
2171 | **iso_format) |
---|
2172 | elif (field_type.startswith('integer') or |
---|
2173 | field_type.startswith('reference ') or |
---|
2174 | field_type.startswith('list:integer') or |
---|
2175 | field_type.startswith('list:reference ')): |
---|
2176 | widget_ = SQLFORM.widgets.integer |
---|
2177 | value_input = widget_.widget( |
---|
2178 | field, field.default, _id=_id, |
---|
2179 | _class=widget_._class + ' form-control') |
---|
2180 | else: |
---|
2181 | value_input = INPUT( |
---|
2182 | _type='text', _id=_id, |
---|
2183 | _class="%s %s" % ((field_type or ''), 'form-control')) |
---|
2184 | |
---|
2185 | if hasattr(field.requires, 'options'): |
---|
2186 | value_input = SELECT( |
---|
2187 | *[OPTION(v, _value=k) |
---|
2188 | for k, v in field.requires.options()], |
---|
2189 | _class='form-control', |
---|
2190 | **dict(_id=_id)) |
---|
2191 | |
---|
2192 | new_button = INPUT( |
---|
2193 | _type="button", _value=T('New Search'), _class="btn btn-default", _title=T('Start building a new search'), |
---|
2194 | _onclick="%s_build_query('new','%s')" % (prefix, field)) |
---|
2195 | and_button = INPUT( |
---|
2196 | _type="button", _value=T('+ And'), _class="btn btn-default", _title=T('Add this to the search as an AND term'), |
---|
2197 | _onclick="%s_build_query('and','%s')" % (prefix, field)) |
---|
2198 | or_button = INPUT( |
---|
2199 | _type="button", _value=T('+ Or'), _class="btn btn-default", _title=T('Add this to the search as an OR term'), |
---|
2200 | _onclick="%s_build_query('or','%s')" % (prefix, field)) |
---|
2201 | close_button = INPUT( |
---|
2202 | _type="button", _value=T('Close'), _class="btn btn-default", |
---|
2203 | _onclick="jQuery('#%s').slideUp()" % panel_id) |
---|
2204 | |
---|
2205 | criteria.append(DIV( |
---|
2206 | operators, value_input, new_button, |
---|
2207 | and_button, or_button, close_button, |
---|
2208 | _id='%s_%s' % (field_id, name), |
---|
2209 | _class='w2p_query_row', |
---|
2210 | _style='display:none')) |
---|
2211 | |
---|
2212 | criteria.insert(0, SELECT( |
---|
2213 | _id=fields_id, |
---|
2214 | _onchange="jQuery('.w2p_query_row').hide();jQuery('#%s_'+jQuery('#%s').val().replace('.','-')).show();" % (field_id, fields_id), |
---|
2215 | _style='float:left', _class='form-control', |
---|
2216 | *selectfields)) |
---|
2217 | |
---|
2218 | fadd = SCRIPT(""" |
---|
2219 | jQuery('#%(fields_id)s input,#%(fields_id)s select').css( |
---|
2220 | 'width','auto'); |
---|
2221 | jQuery(function(){web2py_ajax_fields('#%(fields_id)s');}); |
---|
2222 | function %(prefix)s_build_query(aggregator,a) { |
---|
2223 | var b=a.replace('.','-'); |
---|
2224 | var option = jQuery('#%(field_id)s_'+b+' select').val(); |
---|
2225 | var value; |
---|
2226 | var $value_item = jQuery('#%(value_id)s_'+b); |
---|
2227 | if ($value_item.is(':checkbox')){ |
---|
2228 | if ($value_item.is(':checked')) |
---|
2229 | value = 'True'; |
---|
2230 | else value = 'False'; |
---|
2231 | } |
---|
2232 | else |
---|
2233 | { value = $value_item.val().replace('"','\\\\"')} |
---|
2234 | var s=a+' '+option+' "'+value+'"'; |
---|
2235 | var k=jQuery('#%(keywords_id)s'); |
---|
2236 | var v=k.val(); |
---|
2237 | if(aggregator=='new') k.val(s); else k.val((v?(v+' '+ aggregator +' '):'')+s); |
---|
2238 | } |
---|
2239 | """ % dict( |
---|
2240 | prefix=prefix, fields_id=fields_id, keywords_id=keywords_id, |
---|
2241 | field_id=field_id, value_id=value_id |
---|
2242 | ) |
---|
2243 | ) |
---|
2244 | return CAT( |
---|
2245 | DIV(_id=panel_id, _style="display:none;", *criteria), fadd) |
---|
2246 | |
---|
2247 | @staticmethod |
---|
2248 | def grid(query, |
---|
2249 | fields=None, |
---|
2250 | field_id=None, |
---|
2251 | left=None, |
---|
2252 | headers={}, |
---|
2253 | orderby=None, |
---|
2254 | groupby=None, |
---|
2255 | searchable=True, |
---|
2256 | sortable=True, |
---|
2257 | paginate=20, |
---|
2258 | deletable=True, |
---|
2259 | editable=True, |
---|
2260 | details=True, |
---|
2261 | selectable=None, |
---|
2262 | create=True, |
---|
2263 | csv=True, |
---|
2264 | links=None, |
---|
2265 | links_in_grid=True, |
---|
2266 | upload='<default>', |
---|
2267 | args=[], |
---|
2268 | user_signature=True, |
---|
2269 | maxtextlengths={}, |
---|
2270 | maxtextlength=20, |
---|
2271 | onvalidation=None, |
---|
2272 | onfailure=None, |
---|
2273 | oncreate=None, |
---|
2274 | onupdate=None, |
---|
2275 | ondelete=None, |
---|
2276 | sorter_icons=(XML('▲'), XML('▼')), |
---|
2277 | ui = 'web2py', |
---|
2278 | showbuttontext=True, |
---|
2279 | _class="web2py_grid", |
---|
2280 | formname='web2py_grid', |
---|
2281 | search_widget='default', |
---|
2282 | advanced_search=True, |
---|
2283 | ignore_rw = False, |
---|
2284 | formstyle = None, |
---|
2285 | exportclasses = None, |
---|
2286 | formargs={}, |
---|
2287 | createargs={}, |
---|
2288 | editargs={}, |
---|
2289 | viewargs={}, |
---|
2290 | selectable_submit_button='Submit', |
---|
2291 | buttons_placement = 'right', |
---|
2292 | links_placement = 'right', |
---|
2293 | noconfirm=False, |
---|
2294 | cache_count=None, |
---|
2295 | client_side_delete=False, |
---|
2296 | ignore_common_filters=None, |
---|
2297 | auto_pagination=True, |
---|
2298 | use_cursor=False, |
---|
2299 | represent_none=None, |
---|
2300 | showblobs=False): |
---|
2301 | |
---|
2302 | dbset = None |
---|
2303 | formstyle = formstyle or current.response.formstyle |
---|
2304 | if isinstance(query, Set): |
---|
2305 | query = query.query |
---|
2306 | |
---|
2307 | # jQuery UI ThemeRoller classes (empty if ui is disabled) |
---|
2308 | if ui == 'jquery-ui': |
---|
2309 | ui = dict(widget='ui-widget', |
---|
2310 | header='ui-widget-header', |
---|
2311 | content='ui-widget-content', |
---|
2312 | default='ui-state-default', |
---|
2313 | cornerall='ui-corner-all', |
---|
2314 | cornertop='ui-corner-top', |
---|
2315 | cornerbottom='ui-corner-bottom', |
---|
2316 | button='ui-button-text-icon-primary', |
---|
2317 | buttontext='ui-button-text', |
---|
2318 | buttonadd='ui-icon ui-icon-plusthick', |
---|
2319 | buttonback='ui-icon ui-icon-arrowreturnthick-1-w', |
---|
2320 | buttonexport='ui-icon ui-icon-transferthick-e-w', |
---|
2321 | buttondelete='ui-icon ui-icon-trash', |
---|
2322 | buttonedit='ui-icon ui-icon-pencil', |
---|
2323 | buttontable='ui-icon ui-icon-triangle-1-e', |
---|
2324 | buttonview='ui-icon ui-icon-zoomin', |
---|
2325 | ) |
---|
2326 | elif ui == 'web2py': |
---|
2327 | ui = dict(widget='', |
---|
2328 | header='', |
---|
2329 | content='', |
---|
2330 | default='', |
---|
2331 | cornerall='', |
---|
2332 | cornertop='', |
---|
2333 | cornerbottom='', |
---|
2334 | button='button btn btn-default btn-secondary', |
---|
2335 | buttontext='buttontext button', |
---|
2336 | buttonadd='icon plus icon-plus glyphicon glyphicon-plus', |
---|
2337 | buttonback='icon arrowleft icon-arrow-left glyphicon glyphicon-arrow-left', |
---|
2338 | buttonexport='icon downarrow icon-download glyphicon glyphicon-download', |
---|
2339 | buttondelete='icon trash icon-trash glyphicon glyphicon-trash', |
---|
2340 | buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil', |
---|
2341 | buttontable='icon rightarrow icon-arrow-right glyphicon glyphicon-arrow-right', |
---|
2342 | buttonview='icon magnifier icon-zoom-in glyphicon glyphicon-zoom-in' |
---|
2343 | ) |
---|
2344 | elif not isinstance(ui, dict): |
---|
2345 | raise RuntimeError('SQLFORM.grid ui argument must be a dictionary') |
---|
2346 | |
---|
2347 | db = query._db |
---|
2348 | T = current.T |
---|
2349 | request = current.request |
---|
2350 | session = current.session |
---|
2351 | response = current.response |
---|
2352 | logged = session.auth and session.auth.user |
---|
2353 | wenabled = (not user_signature or logged) and not groupby |
---|
2354 | create = wenabled and create |
---|
2355 | editable = wenabled and editable |
---|
2356 | deletable = wenabled and deletable |
---|
2357 | details = not groupby and details |
---|
2358 | rows = None |
---|
2359 | |
---|
2360 | # see issue 1980. Basically we can have keywords in get_vars |
---|
2361 | # (i.e. when the search term is propagated through page=2&keywords=abc) |
---|
2362 | # but if there is keywords in post_vars (i.e. POSTing a search request) |
---|
2363 | # the one in get_vars should be replaced by the new one |
---|
2364 | keywords = '' |
---|
2365 | if 'keywords' in request.post_vars: |
---|
2366 | keywords = request.post_vars.keywords |
---|
2367 | elif 'keywords' in request.get_vars: |
---|
2368 | keywords = request.get_vars.keywords |
---|
2369 | |
---|
2370 | def fetch_count(dbset): |
---|
2371 | ##FIXME for google:datastore cache_count is ignored |
---|
2372 | ## if it's not an integer |
---|
2373 | if cache_count is None or isinstance(cache_count, tuple): |
---|
2374 | if groupby: |
---|
2375 | c = 'count(*) AS count_all' |
---|
2376 | nrows = db.executesql( |
---|
2377 | 'select count(*) from (%s) _tmp;' % |
---|
2378 | dbset._select(c, left=left, cacheable=True, |
---|
2379 | groupby=groupby, |
---|
2380 | cache=cache_count)[:-1])[0][0] |
---|
2381 | elif left: |
---|
2382 | c = 'count(*)' |
---|
2383 | nrows = dbset.select(c, left=left, cacheable=True, cache=cache_count).first()[c] |
---|
2384 | elif dbset._db._adapter.dbengine == 'google:datastore': |
---|
2385 | # if we don't set a limit, this can timeout for a large table |
---|
2386 | nrows = dbset.db._adapter.count(dbset.query, limit=1000) |
---|
2387 | else: |
---|
2388 | nrows = dbset.count(cache=cache_count) |
---|
2389 | elif isinstance(cache_count, integer_types): |
---|
2390 | nrows = cache_count |
---|
2391 | elif callable(cache_count): |
---|
2392 | nrows = cache_count(dbset, request.vars) |
---|
2393 | else: |
---|
2394 | nrows = 0 |
---|
2395 | return nrows |
---|
2396 | |
---|
2397 | def fix_orderby(orderby): |
---|
2398 | if not auto_pagination: |
---|
2399 | return orderby |
---|
2400 | # enforce always an ORDER clause to avoid |
---|
2401 | # pagination errors. field_id is needed anyhow, |
---|
2402 | # is unique and usually indexed. See issue #679 |
---|
2403 | if not orderby: |
---|
2404 | orderby = field_id |
---|
2405 | elif isinstance(orderby, list): |
---|
2406 | orderby = reduce(lambda a,b: a|b, orderby) |
---|
2407 | elif isinstance(orderby, Field) and orderby is not field_id: |
---|
2408 | # here we're with an ASC order on a field stored as orderby |
---|
2409 | orderby = orderby | field_id |
---|
2410 | elif (isinstance(orderby, Expression) and |
---|
2411 | orderby.first and orderby.first is not field_id): |
---|
2412 | # here we're with a DESC order on a field stored as orderby.first |
---|
2413 | orderby = orderby | field_id |
---|
2414 | return orderby |
---|
2415 | |
---|
2416 | def url(**b): |
---|
2417 | b['args'] = args + b.get('args', []) |
---|
2418 | localvars = request.get_vars.copy() |
---|
2419 | localvars.update(b.get('vars', {})) |
---|
2420 | # avoid empty keywords in vars |
---|
2421 | if localvars.get('keywords') == '': |
---|
2422 | del localvars['keywords'] |
---|
2423 | # avoid propagating order=None in vars |
---|
2424 | if localvars.get('order') == 'None': |
---|
2425 | del localvars['order'] |
---|
2426 | b['vars'] = localvars |
---|
2427 | b['hash_vars'] = False |
---|
2428 | b['user_signature'] = user_signature |
---|
2429 | return URL(**b) |
---|
2430 | |
---|
2431 | def url2(**b): |
---|
2432 | b['args'] = request.args + b.get('args', []) |
---|
2433 | localvars = request.get_vars.copy() |
---|
2434 | localvars.update(b.get('vars', {})) |
---|
2435 | b['vars'] = localvars |
---|
2436 | b['hash_vars'] = False |
---|
2437 | b['user_signature'] = user_signature |
---|
2438 | return URL(**b) |
---|
2439 | |
---|
2440 | referrer = session.get('_web2py_grid_referrer_' + formname, url()) |
---|
2441 | # if not user_signature every action is accessible |
---|
2442 | # else forbid access unless |
---|
2443 | # - url is based url |
---|
2444 | # - url has valid signature (vars are not signed, only path_info) |
---|
2445 | # = url does not contain 'create','delete','edit' (readonly) |
---|
2446 | if user_signature: |
---|
2447 | if not ('/'.join(map(str,args)) == '/'.join(map(str,request.args)) or |
---|
2448 | URL.verify(request, user_signature=user_signature, hash_vars=False) or |
---|
2449 | (request.args(len(args)) == 'view' and not logged)): |
---|
2450 | session.flash = T('not authorized') |
---|
2451 | redirect(referrer) |
---|
2452 | |
---|
2453 | def gridbutton(buttonclass='buttonadd', buttontext=T('Add'), |
---|
2454 | buttonurl=url(args=[]), callback=None, |
---|
2455 | delete=None, noconfirm=None, title=None): |
---|
2456 | if showbuttontext: |
---|
2457 | return A(SPAN(_class=ui.get(buttonclass)), CAT(' '), |
---|
2458 | SPAN(T(buttontext), _title=title or T(buttontext), |
---|
2459 | _class=ui.get('buttontext')), |
---|
2460 | _href=buttonurl, |
---|
2461 | callback=callback, |
---|
2462 | delete=delete, |
---|
2463 | noconfirm=noconfirm, |
---|
2464 | _class=ui.get('button'), |
---|
2465 | cid=request.cid) |
---|
2466 | else: |
---|
2467 | return A(SPAN(_class=ui.get(buttonclass)), |
---|
2468 | _href=buttonurl, |
---|
2469 | callback=callback, |
---|
2470 | delete=delete, |
---|
2471 | noconfirm=noconfirm, |
---|
2472 | _title=title or T(buttontext), |
---|
2473 | _class=ui.get('button'), |
---|
2474 | cid=request.cid) |
---|
2475 | |
---|
2476 | dbset = db(query, ignore_common_filters=ignore_common_filters) |
---|
2477 | tablenames = db._adapter.tables(dbset.query) |
---|
2478 | if left is not None: |
---|
2479 | if not isinstance(left, (list, tuple)): |
---|
2480 | left = [left] |
---|
2481 | for join in left: |
---|
2482 | tablenames = merge_tablemaps(tablenames, db._adapter.tables(join)) |
---|
2483 | tables = [db[tablename] for tablename in tablenames] |
---|
2484 | if fields: |
---|
2485 | columns = [f for f in fields if f.tablename in tablenames and f.listable] |
---|
2486 | else: |
---|
2487 | fields = [] |
---|
2488 | columns = [] |
---|
2489 | filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs) |
---|
2490 | filter2 = lambda f: isinstance(f, Field) and f.readable and f.listable |
---|
2491 | for table in tables: |
---|
2492 | fields += list(filter(filter1, table)) |
---|
2493 | columns += list(filter(filter2, table)) |
---|
2494 | for k, f in iteritems(table): |
---|
2495 | if not k.startswith('_'): |
---|
2496 | if isinstance(f, Field.Virtual) and f.readable: |
---|
2497 | f.tablename = table._tablename |
---|
2498 | fields.append(f) |
---|
2499 | columns.append(f) |
---|
2500 | if not field_id: |
---|
2501 | if groupby is None: |
---|
2502 | field_id = tables[0]._id |
---|
2503 | elif groupby and isinstance(groupby, Field): |
---|
2504 | # take the field passed as groupby |
---|
2505 | field_id = groupby |
---|
2506 | elif groupby and isinstance(groupby, Expression): |
---|
2507 | # take the first groupby field |
---|
2508 | field_id = groupby.first |
---|
2509 | while not(isinstance(field_id, Field)): |
---|
2510 | # Navigate to the first Field of the expression |
---|
2511 | field_id = field_id.first |
---|
2512 | table = field_id.table |
---|
2513 | tablename = table._tablename |
---|
2514 | if not any(str(f) == str(field_id) for f in fields): |
---|
2515 | fields = [f for f in fields] + [field_id] |
---|
2516 | if upload == '<default>': |
---|
2517 | upload = lambda filename: url(args=['download', filename]) |
---|
2518 | if request.args(-2) == 'download': |
---|
2519 | stream = response.download(request, db) |
---|
2520 | raise HTTP(200, stream, **response.headers) |
---|
2521 | |
---|
2522 | def buttons(edit=False, view=False, record=None): |
---|
2523 | buttons = DIV(gridbutton('buttonback', 'Back', referrer), |
---|
2524 | _class='form_header row_buttons %(header)s %(cornertop)s' % ui) |
---|
2525 | if edit and (not callable(edit) or edit(record)): |
---|
2526 | args = ['edit', table._tablename, request.args[-1]] |
---|
2527 | buttons.append(gridbutton('buttonedit', 'Edit', |
---|
2528 | url(args=args))) |
---|
2529 | if view: |
---|
2530 | args = ['view', table._tablename, request.args[-1]] |
---|
2531 | buttons.append(gridbutton('buttonview', 'View', |
---|
2532 | url(args=args))) |
---|
2533 | if record and links: |
---|
2534 | for link in links: |
---|
2535 | if isinstance(link, dict): |
---|
2536 | buttons.append(link['body'](record)) |
---|
2537 | elif link(record): |
---|
2538 | buttons.append(link(record)) |
---|
2539 | return buttons |
---|
2540 | |
---|
2541 | def linsert(lst, i, x): |
---|
2542 | """Internal use only: inserts x list into lst at i pos:: |
---|
2543 | |
---|
2544 | a = [1, 2] |
---|
2545 | linsert(a, 1, [0, 3]) |
---|
2546 | a = [1, 0, 3, 2] |
---|
2547 | """ |
---|
2548 | lst[i:i] = x |
---|
2549 | |
---|
2550 | formfooter = DIV( |
---|
2551 | _class='form_footer row_buttons %(header)s %(cornerbottom)s' % ui) |
---|
2552 | |
---|
2553 | create_form = update_form = view_form = search_form = None |
---|
2554 | |
---|
2555 | if create and request.args(-2) == 'new': |
---|
2556 | table = db[request.args[-1]] |
---|
2557 | sqlformargs = dict(ignore_rw=ignore_rw, formstyle=formstyle, |
---|
2558 | _class='web2py_form') |
---|
2559 | sqlformargs.update(formargs) |
---|
2560 | sqlformargs.update(createargs) |
---|
2561 | create_form = SQLFORM(table, **sqlformargs) |
---|
2562 | create_form.process(formname=formname, |
---|
2563 | next=referrer, |
---|
2564 | onvalidation=onvalidation, |
---|
2565 | onfailure=onfailure, |
---|
2566 | onsuccess=oncreate) |
---|
2567 | res = DIV(buttons(), create_form, formfooter, _class=_class) |
---|
2568 | res.create_form = create_form |
---|
2569 | res.update_form = update_form |
---|
2570 | res.view_form = view_form |
---|
2571 | res.search_form = search_form |
---|
2572 | res.rows = None |
---|
2573 | return res |
---|
2574 | |
---|
2575 | elif details and request.args(-3) == 'view': |
---|
2576 | table = db[request.args[-2]] |
---|
2577 | record = table(request.args[-1]) or redirect(referrer) |
---|
2578 | if represent_none is not None: |
---|
2579 | for field in record.keys(): |
---|
2580 | if record[field] is None: |
---|
2581 | record[field] = represent_none |
---|
2582 | sqlformargs = dict(upload=upload, ignore_rw=ignore_rw, |
---|
2583 | formstyle=formstyle, readonly=True, |
---|
2584 | _class='web2py_form') |
---|
2585 | sqlformargs.update(formargs) |
---|
2586 | sqlformargs.update(viewargs) |
---|
2587 | view_form = SQLFORM(table, record, **sqlformargs) |
---|
2588 | res = DIV(buttons(edit=editable, record=record), view_form, |
---|
2589 | formfooter, _class=_class) |
---|
2590 | res.create_form = create_form |
---|
2591 | res.update_form = update_form |
---|
2592 | res.view_form = view_form |
---|
2593 | res.search_form = search_form |
---|
2594 | res.rows = None |
---|
2595 | return res |
---|
2596 | elif editable and request.args(-3) == 'edit': |
---|
2597 | table = db[request.args[-2]] |
---|
2598 | record = table(request.args[-1]) or redirect(URL('error')) |
---|
2599 | deletable_ = deletable(record) \ |
---|
2600 | if callable(deletable) else deletable |
---|
2601 | sqlformargs = dict(upload=upload, ignore_rw=ignore_rw, |
---|
2602 | formstyle=formstyle, deletable=deletable_, |
---|
2603 | _class='web2py_form', |
---|
2604 | submit_button=T('Submit'), |
---|
2605 | delete_label=T('Check to delete')) |
---|
2606 | sqlformargs.update(formargs) |
---|
2607 | sqlformargs.update(editargs) |
---|
2608 | update_form = SQLFORM(table, record, **sqlformargs) |
---|
2609 | update_form.process( |
---|
2610 | formname=formname, |
---|
2611 | onvalidation=onvalidation, |
---|
2612 | onfailure=onfailure, |
---|
2613 | onsuccess=onupdate, |
---|
2614 | next=referrer) |
---|
2615 | res = DIV(buttons(view=details, record=record), |
---|
2616 | update_form, formfooter, _class=_class) |
---|
2617 | res.create_form = create_form |
---|
2618 | res.update_form = update_form |
---|
2619 | res.view_form = view_form |
---|
2620 | res.search_form = search_form |
---|
2621 | res.rows = None |
---|
2622 | return res |
---|
2623 | elif deletable and request.args(-3) == 'delete': |
---|
2624 | table = db[request.args[-2]] |
---|
2625 | if not callable(deletable): |
---|
2626 | if ondelete: |
---|
2627 | ondelete(table, request.args[-1]) |
---|
2628 | db(table[table._id.name] == request.args[-1]).delete() |
---|
2629 | else: |
---|
2630 | record = table(request.args[-1]) or redirect(URL('error')) |
---|
2631 | if deletable(record): |
---|
2632 | if ondelete: |
---|
2633 | ondelete(table, request.args[-1]) |
---|
2634 | db(table[table._id.name] == request.args[-1]).delete() |
---|
2635 | if request.ajax: |
---|
2636 | # this means javascript is enabled, so we don't need to do |
---|
2637 | # a redirect |
---|
2638 | if not client_side_delete: |
---|
2639 | # if it's an ajax request and we don't need to reload the |
---|
2640 | # entire page, let's just inform that there have been no |
---|
2641 | # exceptions and don't regenerate the grid |
---|
2642 | raise HTTP(200) |
---|
2643 | else: |
---|
2644 | # if it's requested that the grid gets reloaded on delete |
---|
2645 | # on ajax, the redirect should be on the original location |
---|
2646 | newloc = request.env.http_web2py_component_location |
---|
2647 | redirect(newloc, client_side=client_side_delete) |
---|
2648 | else: |
---|
2649 | # we need to do a redirect because javascript is not enabled |
---|
2650 | redirect(referrer, client_side=client_side_delete) |
---|
2651 | |
---|
2652 | exportManager = dict( |
---|
2653 | csv_with_hidden_cols=(ExporterCSV_hidden, 'CSV (hidden cols)', T('Comma-separated export including columns not shown; fields from other tables are exported as raw values for faster export')), |
---|
2654 | csv=(ExporterCSV, 'CSV', T('Comma-separated export of visible columns. Fields from other tables are exported as they appear on-screen but this may be slow for many rows')), |
---|
2655 | xml=(ExporterXML, 'XML', T('XML export of columns shown')), |
---|
2656 | html=(ExporterHTML, 'HTML', T('HTML export of visible columns')), |
---|
2657 | json=(ExporterJSON, 'JSON', T('JSON export of visible columns')), |
---|
2658 | tsv_with_hidden_cols=(ExporterTSV_hidden, 'TSV (Spreadsheets, hidden cols)', T('Spreadsheet-optimised export of tab-separated content including hidden columns. May be slow')), |
---|
2659 | tsv=(ExporterTSV, 'TSV (Spreadsheets)', T('Spreadsheet-optimised export of tab-separated content, visible columns only. May be slow.'))) |
---|
2660 | if exportclasses is not None: |
---|
2661 | """ |
---|
2662 | remember: allow to set exportclasses=dict(csv=False, csv_with_hidden_cols=False) to disable the csv format |
---|
2663 | """ |
---|
2664 | exportManager.update(exportclasses) |
---|
2665 | |
---|
2666 | export_type = request.vars._export_type |
---|
2667 | if export_type: |
---|
2668 | order = request.vars.order or '' |
---|
2669 | if sortable: |
---|
2670 | if order and not order == 'None': |
---|
2671 | otablename, ofieldname = order.split('~')[-1].split('.', 1) |
---|
2672 | sort_field = db[otablename][ofieldname] |
---|
2673 | orderby = sort_field if order[:1] != '~' else ~sort_field |
---|
2674 | |
---|
2675 | orderby = fix_orderby(orderby) |
---|
2676 | |
---|
2677 | # expcolumns start with the visible columns, which |
---|
2678 | # includes visible virtual fields |
---|
2679 | expcolumns = [str(f) for f in columns] |
---|
2680 | selectable_columns = [str(f) for f in columns if not isinstance(f, Field.Virtual)] |
---|
2681 | if export_type.endswith('with_hidden_cols'): |
---|
2682 | for table in tables: |
---|
2683 | for field in table: |
---|
2684 | if field.readable and field.tablename in tablenames: |
---|
2685 | if not str(field) in expcolumns: |
---|
2686 | expcolumns.append(str(field)) |
---|
2687 | if not(isinstance(field, Field.Virtual)): |
---|
2688 | selectable_columns.append(str(field)) |
---|
2689 | # look for virtual fields not displayed (and virtual method |
---|
2690 | # fields to be added here?) |
---|
2691 | for (field_name, field) in iteritems(table): |
---|
2692 | if isinstance(field, Field.Virtual) and not str(field) in expcolumns: |
---|
2693 | expcolumns.append(str(field)) |
---|
2694 | |
---|
2695 | expcolumns = ['"%s"' % '"."'.join(f.split('.')) for f in expcolumns] |
---|
2696 | if export_type in exportManager and exportManager[export_type]: |
---|
2697 | if keywords: |
---|
2698 | try: |
---|
2699 | # the query should be constructed using searchable |
---|
2700 | # fields but not virtual fields |
---|
2701 | is_searchable = lambda f: f.readable and not isinstance(f, Field.Virtual) and f.searchable |
---|
2702 | sfields = reduce(lambda a, b: a + b, [list(filter(is_searchable, t)) for t in tables]) |
---|
2703 | # use custom_query using searchable |
---|
2704 | if callable(searchable): |
---|
2705 | dbset = dbset(searchable(sfields, keywords)) |
---|
2706 | else: |
---|
2707 | dbset = dbset(SQLFORM.build_query( |
---|
2708 | sfields, keywords)) |
---|
2709 | rows = dbset.select(left=left, orderby=orderby, |
---|
2710 | cacheable=True, *selectable_columns) |
---|
2711 | except Exception as e: |
---|
2712 | response.flash = T('Internal Error') |
---|
2713 | rows = [] |
---|
2714 | else: |
---|
2715 | rows = dbset.select(left=left, orderby=orderby, |
---|
2716 | cacheable=True, *selectable_columns) |
---|
2717 | |
---|
2718 | value = exportManager[export_type] |
---|
2719 | clazz = value[0] if hasattr(value, '__getitem__') else value |
---|
2720 | # expcolumns is all cols to be exported including virtual fields |
---|
2721 | rows.colnames = expcolumns |
---|
2722 | oExp = clazz(rows) |
---|
2723 | export_filename = \ |
---|
2724 | request.vars.get('_export_filename') or 'rows' |
---|
2725 | filename = '.'.join((export_filename, oExp.file_ext)) |
---|
2726 | response.headers['Content-Type'] = oExp.content_type |
---|
2727 | response.headers['Content-Disposition'] = \ |
---|
2728 | 'attachment;filename=' + filename + ';' |
---|
2729 | raise HTTP(200, oExp.export(), **response.headers) |
---|
2730 | |
---|
2731 | elif request.vars.records and not isinstance( |
---|
2732 | request.vars.records, list): |
---|
2733 | request.vars.records = [request.vars.records] |
---|
2734 | elif not request.vars.records: |
---|
2735 | request.vars.records = [] |
---|
2736 | |
---|
2737 | session['_web2py_grid_referrer_' + formname] = url2(vars=request.get_vars) |
---|
2738 | console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui) |
---|
2739 | error = None |
---|
2740 | if create: |
---|
2741 | add = gridbutton( |
---|
2742 | buttonclass='buttonadd', |
---|
2743 | buttontext=T('Add Record'), |
---|
2744 | title=T("Add record to database"), |
---|
2745 | buttonurl=url(args=['new', tablename])) |
---|
2746 | if not searchable: |
---|
2747 | console.append(add) |
---|
2748 | else: |
---|
2749 | add = '' |
---|
2750 | |
---|
2751 | if searchable: |
---|
2752 | sfields = reduce(lambda a, b: a + b, |
---|
2753 | [[f for f in t if f.readable and f.searchable] for t in tables]) |
---|
2754 | if isinstance(search_widget, dict): |
---|
2755 | search_widget = search_widget[tablename] |
---|
2756 | if search_widget == 'default': |
---|
2757 | prefix = formname == 'web2py_grid' and 'w2p' or 'w2p_%s' % formname |
---|
2758 | search_menu = SQLFORM.search_menu(sfields, prefix=prefix) |
---|
2759 | spanel_id = '%s_query_fields' % prefix |
---|
2760 | sfields_id = '%s_query_panel' % prefix |
---|
2761 | skeywords_id = '%s_keywords' % prefix |
---|
2762 | # hidden fields to preserve keywords in url after the submit |
---|
2763 | hidden_fields = [INPUT(_type='hidden', _value=v, _name=k) for k, v in request.get_vars.items() if k not in ['keywords', 'page']] |
---|
2764 | search_widget = lambda sfield, url: CAT(FORM( |
---|
2765 | INPUT(_name='keywords', _value=keywords, |
---|
2766 | _id=skeywords_id, _class='form-control', |
---|
2767 | _onfocus="jQuery('#%s').change();jQuery('#%s').slideDown();" % (spanel_id, sfields_id) if advanced_search else '' |
---|
2768 | ), |
---|
2769 | INPUT(_type='submit', _value=T('Search'), _class="btn btn-default btn-secondary"), |
---|
2770 | INPUT(_type='submit', _value=T('Clear'), _class="btn btn-default btn-secondary", |
---|
2771 | _onclick="jQuery('#%s').val('');" % skeywords_id), |
---|
2772 | *hidden_fields, |
---|
2773 | _method="GET", _action=url), search_menu) |
---|
2774 | # TODO vars from the url should be removed, they are not used by the submit |
---|
2775 | form = search_widget and search_widget(sfields, url()) or '' |
---|
2776 | console.append(add) |
---|
2777 | console.append(form) |
---|
2778 | if keywords: |
---|
2779 | try: |
---|
2780 | if callable(searchable): |
---|
2781 | subquery = searchable(sfields, keywords) |
---|
2782 | else: |
---|
2783 | subquery = SQLFORM.build_query(sfields, keywords) |
---|
2784 | except RuntimeError: |
---|
2785 | subquery = None |
---|
2786 | error = T('Invalid query') |
---|
2787 | else: |
---|
2788 | subquery = None |
---|
2789 | else: |
---|
2790 | subquery = None |
---|
2791 | |
---|
2792 | if subquery: |
---|
2793 | dbset = dbset(subquery) |
---|
2794 | try: |
---|
2795 | nrows = fetch_count(dbset) |
---|
2796 | except: |
---|
2797 | nrows = 0 |
---|
2798 | error = T('Unsupported query') |
---|
2799 | |
---|
2800 | order = request.vars.order or '' |
---|
2801 | asc_icon, desc_icon = sorter_icons |
---|
2802 | if sortable: |
---|
2803 | if order and not order == 'None': |
---|
2804 | otablename, ofieldname = order.split('~')[-1].split('.', 1) |
---|
2805 | sort_field = db[otablename][ofieldname] |
---|
2806 | orderby = sort_field if order[:1] != '~' else ~sort_field |
---|
2807 | |
---|
2808 | headcols = [] |
---|
2809 | if selectable: |
---|
2810 | headcols.append(TH(_class=ui.get('default'))) |
---|
2811 | |
---|
2812 | for field in columns: |
---|
2813 | if not field.readable: |
---|
2814 | continue |
---|
2815 | key = str(field) |
---|
2816 | header = headers.get(key, field.label or key) |
---|
2817 | if sortable and not isinstance(field, Field.Virtual): |
---|
2818 | marker = '' |
---|
2819 | inverted = field.type in ('date', 'datetime', 'time') |
---|
2820 | if key == order.lstrip('~'): |
---|
2821 | if inverted: |
---|
2822 | if key == order: |
---|
2823 | key, marker = 'None', asc_icon |
---|
2824 | else: |
---|
2825 | key, marker = order[1:], desc_icon |
---|
2826 | else: |
---|
2827 | if key == order: |
---|
2828 | key, marker = '~' + order, asc_icon |
---|
2829 | else: |
---|
2830 | key, marker = 'None', desc_icon |
---|
2831 | elif inverted and key == str(field): |
---|
2832 | key = '~' + key |
---|
2833 | header = A(header, marker, _href=url(vars=dict( |
---|
2834 | keywords=keywords, |
---|
2835 | order=key)), cid=request.cid) |
---|
2836 | headcols.append(TH(header, _class=ui.get('default'))) |
---|
2837 | |
---|
2838 | toadd = [] |
---|
2839 | left_cols = 0 |
---|
2840 | right_cols = 0 |
---|
2841 | if links and links_in_grid: |
---|
2842 | for link in links: |
---|
2843 | if isinstance(link, dict): |
---|
2844 | toadd.append(TH(link['header'], _class=ui.get('default'))) |
---|
2845 | if links_placement in ['right', 'both']: |
---|
2846 | headcols.extend(toadd) |
---|
2847 | right_cols += len(toadd) |
---|
2848 | if links_placement in ['left', 'both']: |
---|
2849 | linsert(headcols, 0, toadd) |
---|
2850 | left_cols += len(toadd) |
---|
2851 | |
---|
2852 | # Include extra column for buttons if needed. |
---|
2853 | include_buttons_column = ( |
---|
2854 | details or editable or deletable or |
---|
2855 | (links and links_in_grid and |
---|
2856 | not all([isinstance(link, dict) for link in links]))) |
---|
2857 | if include_buttons_column: |
---|
2858 | if buttons_placement in ['right', 'both']: |
---|
2859 | headcols.append(TH(_class=ui.get('default', ''))) |
---|
2860 | right_cols += 1 |
---|
2861 | if buttons_placement in ['left', 'both']: |
---|
2862 | headcols.insert(0, TH(_class=ui.get('default', ''))) |
---|
2863 | left_cols += 1 |
---|
2864 | |
---|
2865 | head = TR(*headcols, **dict(_class=ui.get('header'))) |
---|
2866 | |
---|
2867 | cursor = True |
---|
2868 | # figure out what page we are on to setup the limitby |
---|
2869 | if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: |
---|
2870 | cursor = request.vars.cursor or True |
---|
2871 | limitby = (0, paginate) |
---|
2872 | page = safe_int(request.vars.page or 1, 1) - 1 |
---|
2873 | elif paginate and paginate < nrows: |
---|
2874 | page = safe_int(request.vars.page or 1, 1) - 1 |
---|
2875 | limitby = (paginate * page, paginate * (page + 1)) |
---|
2876 | else: |
---|
2877 | limitby = None |
---|
2878 | |
---|
2879 | orderby = fix_orderby(orderby) |
---|
2880 | |
---|
2881 | try: |
---|
2882 | table_fields = [field for field in fields |
---|
2883 | if (field.tablename in tablenames and |
---|
2884 | not(isinstance(field, Field.Virtual)))] |
---|
2885 | if dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: |
---|
2886 | rows = dbset.select(left=left, orderby=orderby, |
---|
2887 | groupby=groupby, limitby=limitby, |
---|
2888 | reusecursor=cursor, |
---|
2889 | cacheable=True, *table_fields) |
---|
2890 | next_cursor = dbset._db.get('_lastcursor', None) |
---|
2891 | else: |
---|
2892 | rows = dbset.select(left=left, orderby=orderby, |
---|
2893 | groupby=groupby, limitby=limitby, |
---|
2894 | cacheable=True, *table_fields) |
---|
2895 | next_cursor = None |
---|
2896 | except SyntaxError: |
---|
2897 | rows = None |
---|
2898 | next_cursor = None |
---|
2899 | error = T("Query Not Supported") |
---|
2900 | except Exception as e: |
---|
2901 | rows = None |
---|
2902 | next_cursor = None |
---|
2903 | error = T("Query Not Supported: %s") % e |
---|
2904 | |
---|
2905 | message = error |
---|
2906 | if not message and nrows: |
---|
2907 | if dbset._db._adapter.dbengine == 'google:datastore' and nrows >= 1000: |
---|
2908 | message = T('at least %(nrows)s records found') % dict(nrows=nrows) |
---|
2909 | else: |
---|
2910 | message = T('%(nrows)s records found') % dict(nrows=nrows) |
---|
2911 | console.append(DIV(message or '', _class='web2py_counter')) |
---|
2912 | |
---|
2913 | paginator = UL() |
---|
2914 | if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: |
---|
2915 | # this means we may have a large table with an unknown number of rows. |
---|
2916 | page = safe_int(request.vars.page or 1, 1) - 1 |
---|
2917 | paginator.append(LI('page %s' % (page + 1))) |
---|
2918 | if next_cursor: |
---|
2919 | d = dict(page=page + 2, cursor=next_cursor) |
---|
2920 | if order: |
---|
2921 | d['order'] = order |
---|
2922 | # see issue 1980, also at the top of the definition |
---|
2923 | # if keyworkds is in request.vars, we don't need to |
---|
2924 | # copy over the keywords parameter in the links for pagination |
---|
2925 | if 'keywords' in request.vars and not keywords: |
---|
2926 | d['keywords'] = '' |
---|
2927 | elif keywords: |
---|
2928 | d['keywords'] = keywords |
---|
2929 | paginator.append(LI( |
---|
2930 | A('next', _href=url(vars=d), cid=request.cid))) |
---|
2931 | elif paginate and paginate < nrows: |
---|
2932 | npages, reminder = divmod(nrows, paginate) |
---|
2933 | if reminder: |
---|
2934 | npages += 1 |
---|
2935 | page = safe_int(request.vars.page or 1, 1) - 1 |
---|
2936 | |
---|
2937 | def self_link(name, p): |
---|
2938 | d = dict(page=p + 1) |
---|
2939 | if order: |
---|
2940 | d['order'] = order |
---|
2941 | # see issue 1980, also at the top of the definition |
---|
2942 | # if keyworkds is in request.vars, we don't need to |
---|
2943 | # copy over the keywords parameter in the links for pagination |
---|
2944 | if 'keywords' in request.vars and not keywords: |
---|
2945 | d['keywords'] = '' |
---|
2946 | elif keywords: |
---|
2947 | d['keywords'] = keywords |
---|
2948 | return A(name, _href=url(vars=d), cid=request.cid) |
---|
2949 | NPAGES = 5 # window is 2*NPAGES |
---|
2950 | if page > NPAGES + 1: |
---|
2951 | paginator.append(LI(self_link('<<', 0))) |
---|
2952 | if page > NPAGES: |
---|
2953 | paginator.append(LI(self_link('<', page - 1))) |
---|
2954 | pages = list(range(max(0, page - NPAGES), min(page + NPAGES, npages))) |
---|
2955 | for p in pages: |
---|
2956 | if p == page: |
---|
2957 | paginator.append(LI(A(p + 1, _onclick='return false'), |
---|
2958 | _class='current')) |
---|
2959 | else: |
---|
2960 | paginator.append(LI(self_link(p + 1, p))) |
---|
2961 | if page < npages - NPAGES: |
---|
2962 | paginator.append(LI(self_link('>', page + 1))) |
---|
2963 | if page < npages - NPAGES - 1: |
---|
2964 | paginator.append(LI(self_link('>>', npages - 1))) |
---|
2965 | else: |
---|
2966 | limitby = None |
---|
2967 | |
---|
2968 | if rows: |
---|
2969 | cols = [COL(_id=str(c).replace('.', '-'), |
---|
2970 | data={'column': left_cols + i + 1}) |
---|
2971 | for i, c in enumerate(columns)] |
---|
2972 | cols = [COL(data={'column': i + 1}) for i in range(left_cols)] + \ |
---|
2973 | cols + \ |
---|
2974 | [COL(data={'column': left_cols + len(cols) + i + 1}) |
---|
2975 | for i in range(right_cols)] |
---|
2976 | htmltable = TABLE(COLGROUP(*cols), THEAD(head)) |
---|
2977 | tbody = TBODY() |
---|
2978 | numrec = 0 |
---|
2979 | repr_cache = CacheRepresenter() |
---|
2980 | for row in rows: |
---|
2981 | trcols = [] |
---|
2982 | id = row[field_id] |
---|
2983 | if selectable: |
---|
2984 | trcols.append( |
---|
2985 | INPUT(_type="checkbox", _name="records", _value=id, |
---|
2986 | value=request.vars.records)) |
---|
2987 | for field in columns: |
---|
2988 | if not field.readable: |
---|
2989 | continue |
---|
2990 | elif field.type == 'blob' and not showblobs: |
---|
2991 | continue |
---|
2992 | if isinstance(field, Field.Virtual): |
---|
2993 | try: |
---|
2994 | # fast path, works for joins |
---|
2995 | value = row[field.tablename][field.name] |
---|
2996 | except KeyError: |
---|
2997 | value = dbset.db[field.tablename][row[field_id]][field.name] |
---|
2998 | else: |
---|
2999 | value = row[str(field)] |
---|
3000 | maxlength = maxtextlengths.get(str(field), maxtextlength) |
---|
3001 | if field.represent: |
---|
3002 | if field.type.startswith('reference'): |
---|
3003 | nvalue = repr_cache(field, value, row) |
---|
3004 | else: |
---|
3005 | try: |
---|
3006 | nvalue = field.represent(value, row) |
---|
3007 | except KeyError: |
---|
3008 | try: |
---|
3009 | nvalue = field.represent(value, row[field.tablename]) |
---|
3010 | except KeyError: |
---|
3011 | nvalue = None |
---|
3012 | value = nvalue |
---|
3013 | elif field.type == 'boolean': |
---|
3014 | value = INPUT(_type="checkbox", _checked=value, |
---|
3015 | _disabled=True) |
---|
3016 | elif field.type == 'upload': |
---|
3017 | if value: |
---|
3018 | if callable(upload): |
---|
3019 | value = A( |
---|
3020 | T('file'), _href=upload(value)) |
---|
3021 | elif upload: |
---|
3022 | value = A(T('file'), |
---|
3023 | _href='%s/%s' % (upload, value)) |
---|
3024 | else: |
---|
3025 | value = '' |
---|
3026 | elif isinstance(field.type, SQLCustomType) and callable(field.type.represent): |
---|
3027 | # SQLCustomType has a represent, use it |
---|
3028 | value = field.type.represent(value, row) |
---|
3029 | if isinstance(value, str): |
---|
3030 | value = truncate_string(value, maxlength) |
---|
3031 | elif not isinstance(value, XmlComponent): |
---|
3032 | value = field.formatter(value) |
---|
3033 | if value is None: |
---|
3034 | value = represent_none |
---|
3035 | trcols.append(TD(value)) |
---|
3036 | row_buttons = TD(_class='row_buttons', _nowrap=True) |
---|
3037 | if links and links_in_grid: |
---|
3038 | toadd = [] |
---|
3039 | for link in links: |
---|
3040 | if isinstance(link, dict): |
---|
3041 | toadd.append(TD(link['body'](row))) |
---|
3042 | else: |
---|
3043 | if link(row): |
---|
3044 | row_buttons.append(link(row)) |
---|
3045 | if links_placement in ['right', 'both']: |
---|
3046 | trcols.extend(toadd) |
---|
3047 | if links_placement in ['left', 'both']: |
---|
3048 | linsert(trcols, 0, toadd) |
---|
3049 | |
---|
3050 | if include_buttons_column: |
---|
3051 | if details and (not callable(details) or details(row)): |
---|
3052 | row_buttons.append(gridbutton( |
---|
3053 | 'buttonview', 'View', |
---|
3054 | url(args=['view', tablename, id]))) |
---|
3055 | if editable and (not callable(editable) or editable(row)): |
---|
3056 | row_buttons.append(gridbutton( |
---|
3057 | 'buttonedit', 'Edit', |
---|
3058 | url(args=['edit', tablename, id]))) |
---|
3059 | if deletable and (not callable(deletable) or deletable(row)): |
---|
3060 | row_buttons.append(gridbutton( |
---|
3061 | 'buttondelete', 'Delete', |
---|
3062 | url(args=['delete', tablename, id]), |
---|
3063 | callback=url(args=['delete', tablename, id]), |
---|
3064 | noconfirm=noconfirm, |
---|
3065 | delete='tr')) |
---|
3066 | if buttons_placement in ['right', 'both']: |
---|
3067 | trcols.append(row_buttons) |
---|
3068 | if buttons_placement in ['left', 'both']: |
---|
3069 | trcols.insert(0, row_buttons) |
---|
3070 | if numrec % 2 == 1: |
---|
3071 | classtr = 'w2p_even even' |
---|
3072 | else: |
---|
3073 | classtr = 'w2p_odd odd' |
---|
3074 | numrec += 1 |
---|
3075 | if id: |
---|
3076 | rid = id |
---|
3077 | if callable(rid): # can this ever be callable? |
---|
3078 | rid = rid(row) |
---|
3079 | tr = TR(*trcols, **dict( |
---|
3080 | _id=rid, |
---|
3081 | _class='%s %s' % (classtr, 'with_id'))) |
---|
3082 | else: |
---|
3083 | tr = TR(*trcols, **dict(_class=classtr)) |
---|
3084 | tbody.append(tr) |
---|
3085 | htmltable.append(tbody) |
---|
3086 | htmltable = DIV( |
---|
3087 | htmltable, _class='web2py_htmltable', |
---|
3088 | _style='width:100%;overflow-x:auto;-ms-overflow-x:scroll') |
---|
3089 | if selectable: |
---|
3090 | if not callable(selectable): |
---|
3091 | # now expect that selectable and related parameters are |
---|
3092 | # iterator (list, tuple, etc) |
---|
3093 | inputs = [] |
---|
3094 | for i, submit_info in enumerate(selectable): |
---|
3095 | submit_text = submit_info[0] |
---|
3096 | submit_class = submit_info[2] if len(submit_info) > 2 else '' |
---|
3097 | |
---|
3098 | input_ctrl = INPUT(_type="submit", _name='submit_%d' % i, _value=T(submit_text)) |
---|
3099 | input_ctrl.add_class(submit_class) |
---|
3100 | inputs.append(input_ctrl) |
---|
3101 | else: |
---|
3102 | inputs = [INPUT(_type="submit", _value=T(selectable_submit_button))] |
---|
3103 | |
---|
3104 | if formstyle == 'bootstrap': |
---|
3105 | # add space between buttons |
---|
3106 | htmltable = FORM(htmltable, DIV(_class='form-actions', *inputs)) |
---|
3107 | elif not callable(formstyle) and 'bootstrap' in formstyle: # Same for bootstrap 3 & 4 |
---|
3108 | htmltable = FORM(htmltable, DIV(_class='form-group web2py_table_selectable_actions', *inputs)) |
---|
3109 | else: |
---|
3110 | htmltable = FORM(htmltable, *inputs) |
---|
3111 | |
---|
3112 | if htmltable.process(formname=formname).accepted: |
---|
3113 | htmltable.vars.records = htmltable.vars.records or [] |
---|
3114 | htmltable.vars.records = htmltable.vars.records if isinstance(htmltable.vars.records, list) else [htmltable.vars.records] |
---|
3115 | records = [int(r) for r in htmltable.vars.records] |
---|
3116 | if not callable(selectable): |
---|
3117 | for i, submit_info in enumerate(selectable): |
---|
3118 | submit_callback = submit_info[1] |
---|
3119 | if htmltable.vars.get('submit_%d' % i, False): |
---|
3120 | submit_callback(records) |
---|
3121 | break |
---|
3122 | else: |
---|
3123 | selectable(records) |
---|
3124 | redirect(referrer) |
---|
3125 | else: |
---|
3126 | htmltable = DIV(T('No records found')) |
---|
3127 | |
---|
3128 | if csv and nrows: |
---|
3129 | export_links = [] |
---|
3130 | for k, v in sorted(exportManager.items()): |
---|
3131 | if not v: |
---|
3132 | continue |
---|
3133 | if hasattr(v, "__getitem__"): |
---|
3134 | label = v[1] |
---|
3135 | title = v[2] if len(v) > 2 else label |
---|
3136 | else: |
---|
3137 | label = title = k |
---|
3138 | link = url2(vars=dict( |
---|
3139 | order=request.vars.order or '', |
---|
3140 | _export_type=k, |
---|
3141 | keywords=keywords)) |
---|
3142 | export_links.append(A(T(label), _href=link, _title=title, _class='btn btn-default btn-secondary')) |
---|
3143 | export_menu = \ |
---|
3144 | DIV(T('Export:'), _class="w2p_export_menu", *export_links) |
---|
3145 | else: |
---|
3146 | export_menu = None |
---|
3147 | |
---|
3148 | res = DIV(console, DIV(htmltable, _class="web2py_table"), |
---|
3149 | _class='%s %s' % (_class, ui.get('widget'))) |
---|
3150 | if paginator.components: |
---|
3151 | res.append( |
---|
3152 | DIV(paginator, |
---|
3153 | _class="web2py_paginator %(header)s %(cornerbottom)s" % ui)) |
---|
3154 | if export_menu: |
---|
3155 | res.append(export_menu) |
---|
3156 | res.create_form = create_form |
---|
3157 | res.update_form = update_form |
---|
3158 | res.view_form = view_form |
---|
3159 | res.search_form = search_form |
---|
3160 | res.rows = rows |
---|
3161 | res.dbset = dbset |
---|
3162 | return res |
---|
3163 | |
---|
3164 | @staticmethod |
---|
3165 | def smartgrid(table, constraints=None, linked_tables=None, |
---|
3166 | links=None, links_in_grid=True, |
---|
3167 | args=None, user_signature=True, |
---|
3168 | divider=2*unichr(160) + '>' + 2*unichr(160), breadcrumbs_class='', |
---|
3169 | **kwargs): |
---|
3170 | """ |
---|
3171 | Builds a system of SQLFORM.grid(s) between any referenced tables |
---|
3172 | |
---|
3173 | Args: |
---|
3174 | table: main table |
---|
3175 | constraints(dict): `{'table':query}` that limits which records can |
---|
3176 | be accessible |
---|
3177 | links(dict): like `{'tablename':[lambda row: A(....), ...]}` that |
---|
3178 | will add buttons when table tablename is displayed |
---|
3179 | linked_tables(list): list of tables to be linked |
---|
3180 | |
---|
3181 | Example: |
---|
3182 | given you defined a model as:: |
---|
3183 | |
---|
3184 | db.define_table('person', Field('name'), format='%(name)s') |
---|
3185 | db.define_table('dog', |
---|
3186 | Field('name'), Field('owner', db.person), format='%(name)s') |
---|
3187 | db.define_table('comment', Field('body'), Field('dog', db.dog)) |
---|
3188 | if db(db.person).isempty(): |
---|
3189 | from gluon.contrib.populate import populate |
---|
3190 | populate(db.person, 300) |
---|
3191 | populate(db.dog, 300) |
---|
3192 | populate(db.comment, 1000) |
---|
3193 | |
---|
3194 | in a controller, you can do:: |
---|
3195 | |
---|
3196 | @auth.requires_login() |
---|
3197 | def index(): |
---|
3198 | form=SQLFORM.smartgrid(db[request.args(0) or 'person']) |
---|
3199 | return dict(form=form) |
---|
3200 | |
---|
3201 | """ |
---|
3202 | request = current.request |
---|
3203 | T = current.T |
---|
3204 | if args is None: |
---|
3205 | args = [] |
---|
3206 | |
---|
3207 | def url(**b): |
---|
3208 | b['args'] = request.args[:nargs] + b.get('args', []) |
---|
3209 | b['hash_vars'] = False |
---|
3210 | b['user_signature'] = user_signature |
---|
3211 | return URL(**b) |
---|
3212 | |
---|
3213 | db = table._db |
---|
3214 | breadcrumbs = [] |
---|
3215 | if request.args(len(args)) != table._tablename: |
---|
3216 | request.args[:] = args + [table._tablename] |
---|
3217 | if links is None: |
---|
3218 | links = {} |
---|
3219 | if constraints is None: |
---|
3220 | constraints = {} |
---|
3221 | field = name = None |
---|
3222 | |
---|
3223 | def format(table, row): |
---|
3224 | if not row: |
---|
3225 | return T('Unknown') |
---|
3226 | elif isinstance(table._format, str): |
---|
3227 | return table._format % row |
---|
3228 | elif callable(table._format): |
---|
3229 | return table._format(row) |
---|
3230 | else: |
---|
3231 | return '#' + str(row.id) |
---|
3232 | |
---|
3233 | def plural(table): |
---|
3234 | return table._plural or pluralize(table._singular.lower()).capitalize() |
---|
3235 | |
---|
3236 | try: |
---|
3237 | nargs = len(args) + 1 |
---|
3238 | previous_tablename = table._tablename |
---|
3239 | previous_fieldname = previous_id = None |
---|
3240 | while len(request.args) > nargs: |
---|
3241 | key = request.args(nargs) |
---|
3242 | if '.' in key: |
---|
3243 | id = request.args(nargs + 1) |
---|
3244 | tablename, fieldname = key.split('.', 1) |
---|
3245 | table = db[tablename] |
---|
3246 | field = table[fieldname] |
---|
3247 | field.default = id |
---|
3248 | referee = field.type[10:] |
---|
3249 | if referee != previous_tablename: |
---|
3250 | raise HTTP(400) |
---|
3251 | cond = constraints.get(referee, None) |
---|
3252 | if cond: |
---|
3253 | record = db( |
---|
3254 | db[referee]._id == id)(cond).select().first() |
---|
3255 | else: |
---|
3256 | record = db[referee](id) |
---|
3257 | if previous_id: |
---|
3258 | if record[previous_fieldname] != int(previous_id): |
---|
3259 | raise HTTP(400) |
---|
3260 | previous_tablename = tablename |
---|
3261 | previous_fieldname = fieldname |
---|
3262 | previous_id = id |
---|
3263 | name = format(db[referee], record) |
---|
3264 | breadcrumbs.append( |
---|
3265 | LI(A(T(plural(db[referee])), |
---|
3266 | cid=request.cid, |
---|
3267 | _href=url()), |
---|
3268 | SPAN(divider, _class='divider'), |
---|
3269 | _class='w2p_grid_breadcrumb_elem')) |
---|
3270 | if kwargs.get('details', True): |
---|
3271 | breadcrumbs.append( |
---|
3272 | LI(A(name, cid=request.cid, |
---|
3273 | _href=url(args=['view', referee, id])), |
---|
3274 | SPAN(divider, _class='divider'), |
---|
3275 | _class='w2p_grid_breadcrumb_elem')) |
---|
3276 | nargs += 2 |
---|
3277 | else: |
---|
3278 | break |
---|
3279 | if nargs > len(args) + 1: |
---|
3280 | query = (field == id) |
---|
3281 | # cjk |
---|
3282 | # if isinstance(linked_tables, dict): |
---|
3283 | # linked_tables = linked_tables.get(table._tablename, []) |
---|
3284 | if linked_tables is None or referee in linked_tables: |
---|
3285 | field.represent = (lambda id, r=None, referee=referee, rep=field.represent: |
---|
3286 | A(callable(rep) and rep(id) or id, |
---|
3287 | cid=request.cid, _href=url(args=['view', referee, id]))) |
---|
3288 | except (KeyError, ValueError, TypeError): |
---|
3289 | redirect(URL(args=table._tablename)) |
---|
3290 | if nargs == len(args) + 1: |
---|
3291 | query = table._db._adapter.id_query(table) |
---|
3292 | |
---|
3293 | # filter out data info for displayed table |
---|
3294 | if table._tablename in constraints: |
---|
3295 | query = query & constraints[table._tablename] |
---|
3296 | if isinstance(links, dict): |
---|
3297 | links = links.get(table._tablename, []) |
---|
3298 | for key in ('fields', 'field_id', 'left', 'headers', 'orderby', 'groupby', 'searchable', |
---|
3299 | 'sortable', 'paginate', 'deletable', 'editable', 'details', 'selectable', |
---|
3300 | 'create', 'csv', 'upload', 'maxtextlengths', |
---|
3301 | 'maxtextlength', 'onvalidation', 'onfailure', 'oncreate', 'onupdate', |
---|
3302 | 'ondelete', 'sorter_icons', 'ui', 'showbuttontext', '_class', 'formname', |
---|
3303 | 'search_widget', 'advanced_search', 'ignore_rw', 'formstyle', 'exportclasses', |
---|
3304 | 'formargs', 'createargs', 'editargs', 'viewargs', 'selectable_submit_button', |
---|
3305 | 'buttons_placement', 'links_placement', 'noconfirm', 'cache_count', 'client_side_delete', |
---|
3306 | 'ignore_common_filters', 'auto_pagination', 'use_cursor' |
---|
3307 | ): |
---|
3308 | if isinstance(kwargs.get(key, None), dict): |
---|
3309 | if table._tablename in kwargs[key]: |
---|
3310 | kwargs[key] = kwargs[key][table._tablename] |
---|
3311 | elif '*' in kwargs[key]: |
---|
3312 | kwargs[key] = kwargs[key]['*'] |
---|
3313 | else: |
---|
3314 | del kwargs[key] |
---|
3315 | check = {} |
---|
3316 | id_field_name = table._id.name |
---|
3317 | for rfield in table._referenced_by: |
---|
3318 | check[rfield.tablename] = \ |
---|
3319 | check.get(rfield.tablename, []) + [rfield.name] |
---|
3320 | if linked_tables is None: |
---|
3321 | linked_tables = db.tables() |
---|
3322 | if isinstance(linked_tables, dict): |
---|
3323 | linked_tables = linked_tables.get(table._tablename, []) |
---|
3324 | |
---|
3325 | linked = [] |
---|
3326 | if linked_tables: |
---|
3327 | for item in linked_tables: |
---|
3328 | tb = None |
---|
3329 | if isinstance(item, Table) and item._tablename in check: |
---|
3330 | tablename = item._tablename |
---|
3331 | linked_fieldnames = check[tablename] |
---|
3332 | tb = item |
---|
3333 | elif isinstance(item, str) and item in check: |
---|
3334 | tablename = item |
---|
3335 | linked_fieldnames = check[item] |
---|
3336 | tb = db[item] |
---|
3337 | elif isinstance(item, Field) and item.name in check.get(item._tablename, []): |
---|
3338 | tablename = item._tablename |
---|
3339 | linked_fieldnames = [item.name] |
---|
3340 | tb = item.table |
---|
3341 | else: |
---|
3342 | linked_fieldnames = [] |
---|
3343 | if tb: |
---|
3344 | multiple_links = len(linked_fieldnames) > 1 |
---|
3345 | for fieldname in linked_fieldnames: |
---|
3346 | t = T(plural(tb)) if not multiple_links else \ |
---|
3347 | T("%s(%s)" % (plural(tb), fieldname)) |
---|
3348 | args0 = tablename + '.' + fieldname |
---|
3349 | linked.append( |
---|
3350 | lambda row, t=t, nargs=nargs, args0=args0: |
---|
3351 | A(SPAN(t), cid=request.cid, _href=url( |
---|
3352 | args=[args0, row[id_field_name]]))) |
---|
3353 | links += linked |
---|
3354 | grid = SQLFORM.grid(query, args=request.args[:nargs], links=links, |
---|
3355 | links_in_grid=links_in_grid, |
---|
3356 | user_signature=user_signature, **kwargs) |
---|
3357 | |
---|
3358 | if isinstance(grid, DIV): |
---|
3359 | header = plural(table) |
---|
3360 | next = grid.create_form or grid.update_form or grid.view_form |
---|
3361 | breadcrumbs.append(LI( |
---|
3362 | A(T(header), cid=request.cid, _href=url()), |
---|
3363 | SPAN(divider, _class='divider') if next else '', |
---|
3364 | _class='active w2p_grid_breadcrumb_elem')) |
---|
3365 | if grid.create_form: |
---|
3366 | header = T('New %(entity)s') % dict(entity=table._singular) |
---|
3367 | elif grid.update_form: |
---|
3368 | header = T('Edit %(entity)s') % dict( |
---|
3369 | entity=format(grid.update_form.table, |
---|
3370 | grid.update_form.record)) |
---|
3371 | elif grid.view_form: |
---|
3372 | header = T('View %(entity)s') % dict( |
---|
3373 | entity=format(grid.view_form.table, |
---|
3374 | grid.view_form.record)) |
---|
3375 | if next: |
---|
3376 | breadcrumbs.append(LI( |
---|
3377 | A(T(header), cid=request.cid, _href=url()), |
---|
3378 | _class='active w2p_grid_breadcrumb_elem')) |
---|
3379 | grid.insert( |
---|
3380 | 0, DIV(UL(*breadcrumbs, **{'_class': breadcrumbs_class}), |
---|
3381 | _class='web2py_breadcrumbs')) |
---|
3382 | return grid |
---|
3383 | |
---|
3384 | |
---|
3385 | class SQLTABLE(TABLE): |
---|
3386 | |
---|
3387 | """ |
---|
3388 | Given with a Rows object, as returned by a `db().select()`, generates |
---|
3389 | an html table with the rows. |
---|
3390 | |
---|
3391 | Args: |
---|
3392 | sqlrows : the `Rows` object |
---|
3393 | linkto: URL (or lambda to generate a URL) to edit individual records |
---|
3394 | upload: URL to download uploaded files |
---|
3395 | orderby: Add an orderby link to column headers. |
---|
3396 | headers: dictionary of headers to headers redefinions |
---|
3397 | headers can also be a string to generate the headers from data |
---|
3398 | for now only headers="fieldname:capitalize", |
---|
3399 | headers="labels" and headers=None are supported |
---|
3400 | truncate: length at which to truncate text in table cells. |
---|
3401 | Defaults to 16 characters. |
---|
3402 | columns: a list or dict contaning the names of the columns to be shown |
---|
3403 | Defaults to all |
---|
3404 | th_link: base link to support orderby headers |
---|
3405 | extracolumns: a list of dicts |
---|
3406 | selectid: The id you want to select |
---|
3407 | renderstyle: Boolean render the style with the table |
---|
3408 | cid: use this cid for all links |
---|
3409 | colgroup: #FIXME |
---|
3410 | |
---|
3411 | |
---|
3412 | Extracolumns example |
---|
3413 | :: |
---|
3414 | |
---|
3415 | [{'label':A('Extra', _href='#'), |
---|
3416 | 'class': '', #class name of the header |
---|
3417 | 'width':'', #width in pixels or % |
---|
3418 | 'content':lambda row, rc: A('Edit', _href='edit/%s'%row.id), |
---|
3419 | 'selected': False #agregate class selected to this column}] |
---|
3420 | |
---|
3421 | |
---|
3422 | """ |
---|
3423 | |
---|
3424 | REGEX_ALIAS_MATCH = '^(.*) AS (.*)$' |
---|
3425 | |
---|
3426 | def __init__(self, |
---|
3427 | sqlrows, |
---|
3428 | linkto=None, |
---|
3429 | upload=None, |
---|
3430 | orderby=None, |
---|
3431 | headers={}, |
---|
3432 | truncate=16, |
---|
3433 | columns=None, |
---|
3434 | th_link='', |
---|
3435 | extracolumns=None, |
---|
3436 | selectid=None, |
---|
3437 | renderstyle=False, |
---|
3438 | cid=None, |
---|
3439 | colgroup=False, |
---|
3440 | **attributes |
---|
3441 | ): |
---|
3442 | |
---|
3443 | TABLE.__init__(self, **attributes) |
---|
3444 | |
---|
3445 | self.components = [] |
---|
3446 | self.attributes = attributes |
---|
3447 | self.sqlrows = sqlrows |
---|
3448 | (components, row) = (self.components, []) |
---|
3449 | if not sqlrows: |
---|
3450 | return |
---|
3451 | fieldlist = sqlrows.colnames_fields |
---|
3452 | fieldmap = dict(zip(sqlrows.colnames, fieldlist)) |
---|
3453 | if columns: |
---|
3454 | tablenames = [] |
---|
3455 | for colname, field in fieldmap.items(): |
---|
3456 | if isinstance(field, (Field, Field.Virtual)): |
---|
3457 | tablenames.append(field.tablename) |
---|
3458 | elif isinstance(field, Expression): |
---|
3459 | tablenames.append(field._table._tablename) |
---|
3460 | for tablename in set(tablenames): |
---|
3461 | table = sqlrows.db[tablename] |
---|
3462 | fieldmap.update((("%s.%s" % (tablename, f.name), f) for f in table._virtual_fields + table._virtual_methods)) |
---|
3463 | else: |
---|
3464 | columns = list(sqlrows.colnames) |
---|
3465 | field_types = (Field, Field.Virtual, Field.Method) |
---|
3466 | |
---|
3467 | header_func = { |
---|
3468 | 'fieldname:capitalize': lambda f: f.name.replace('_', ' ').title(), |
---|
3469 | 'labels': lambda f: f.label |
---|
3470 | } |
---|
3471 | if isinstance(headers, str) and headers in header_func: |
---|
3472 | make_name = header_func[headers] |
---|
3473 | headers = {} |
---|
3474 | for c in columns: |
---|
3475 | f = fieldmap.get(c) |
---|
3476 | if isinstance(f, field_types): |
---|
3477 | headers[c] = make_name(f) |
---|
3478 | else: |
---|
3479 | headers[c] = re.sub(self.REGEX_ALIAS_MATCH, r'\2', c) |
---|
3480 | if colgroup: |
---|
3481 | cols = [COL(_id=c.replace('.', '-'), data={'column': i + 1}) |
---|
3482 | for i, c in enumerate(columns)] |
---|
3483 | if extracolumns: |
---|
3484 | cols += [COL(data={'column': len(cols) + i + 1}) |
---|
3485 | for i, c in enumerate(extracolumns)] |
---|
3486 | components.append(COLGROUP(*cols)) |
---|
3487 | |
---|
3488 | if headers is None: |
---|
3489 | headers = {} |
---|
3490 | else: |
---|
3491 | for c in columns: # new implement dict |
---|
3492 | c = str(c) |
---|
3493 | if isinstance(headers.get(c, c), dict): |
---|
3494 | coldict = headers.get(c, c) |
---|
3495 | attrcol = dict() |
---|
3496 | if coldict['width'] != "": |
---|
3497 | attrcol.update(_width=coldict['width']) |
---|
3498 | if coldict['class'] != "": |
---|
3499 | attrcol.update(_class=coldict['class']) |
---|
3500 | row.append(TH(coldict['label'], **attrcol)) |
---|
3501 | elif orderby: |
---|
3502 | row.append(TH(A(headers.get(c, c), |
---|
3503 | _href=th_link + '?orderby=' + c, cid=cid))) |
---|
3504 | else: |
---|
3505 | row.append(TH(headers.get(c, re.sub(self.REGEX_ALIAS_MATCH, r'\2', c)))) |
---|
3506 | |
---|
3507 | if extracolumns: # new implement dict |
---|
3508 | for c in extracolumns: |
---|
3509 | attrcol = dict() |
---|
3510 | if c['width'] != "": |
---|
3511 | attrcol.update(_width=c['width']) |
---|
3512 | if c['class'] != "": |
---|
3513 | attrcol.update(_class=c['class']) |
---|
3514 | row.append(TH(c['label'], **attrcol)) |
---|
3515 | |
---|
3516 | components.append(THEAD(TR(*row))) |
---|
3517 | |
---|
3518 | tbody = [] |
---|
3519 | repr_cache = {} |
---|
3520 | for (rc, record) in enumerate(sqlrows): |
---|
3521 | row = [] |
---|
3522 | if rc % 2 == 1: |
---|
3523 | _class = 'w2p_even even' |
---|
3524 | else: |
---|
3525 | _class = 'w2p_odd odd' |
---|
3526 | |
---|
3527 | if selectid is not None: # new implement |
---|
3528 | if record.get('id') == selectid: |
---|
3529 | _class += ' rowselected' |
---|
3530 | |
---|
3531 | for colname in columns: |
---|
3532 | field = fieldmap.get(colname) |
---|
3533 | if not isinstance(field, field_types): |
---|
3534 | if "_extra" in record and colname in record._extra: |
---|
3535 | r = record._extra[colname] |
---|
3536 | row.append(TD(r)) |
---|
3537 | continue |
---|
3538 | else: |
---|
3539 | raise KeyError( |
---|
3540 | "Column %s not found (SQLTABLE)" % colname) |
---|
3541 | # Virtual fields don't have parent table name... |
---|
3542 | tablename = colname.split('.', 1)[0] |
---|
3543 | fieldname = field.name |
---|
3544 | if tablename in record \ |
---|
3545 | and isinstance(record, Row) \ |
---|
3546 | and isinstance(record[tablename], Row): |
---|
3547 | r = record[tablename][fieldname] |
---|
3548 | elif fieldname in record: |
---|
3549 | r = record[fieldname] |
---|
3550 | else: |
---|
3551 | raise SyntaxError('something wrong in Rows object') |
---|
3552 | r_old = r |
---|
3553 | if not field or isinstance(field, (Field.Virtual, Field.Lazy)): |
---|
3554 | pass |
---|
3555 | elif linkto and field.type == 'id': |
---|
3556 | try: |
---|
3557 | href = linkto(r, 'table', tablename) |
---|
3558 | except TypeError: |
---|
3559 | href = '%s/%s/%s' % (linkto, tablename, r_old) |
---|
3560 | r = A(r, _href=href) |
---|
3561 | elif isinstance(field.type, str) and field.type.startswith('reference'): |
---|
3562 | if linkto: |
---|
3563 | ref = field.type[10:] |
---|
3564 | try: |
---|
3565 | href = linkto(r, 'reference', ref) |
---|
3566 | except TypeError: |
---|
3567 | href = '%s/%s/%s' % (linkto, ref, r_old) |
---|
3568 | if ref.find('.') >= 0: |
---|
3569 | tref, fref = ref.split('.') |
---|
3570 | if hasattr(sqlrows.db[tref], '_primarykey'): |
---|
3571 | href = '%s/%s?%s' % (linkto, tref, urlencode({fref: r})) |
---|
3572 | r = A(represent(field, r, record), _href=str(href)) |
---|
3573 | elif field.represent: |
---|
3574 | if field not in repr_cache: |
---|
3575 | repr_cache[field] = {} |
---|
3576 | if r not in repr_cache[field]: |
---|
3577 | repr_cache[field][r] = represent(field, r, record) |
---|
3578 | r = repr_cache[field][r] |
---|
3579 | elif linkto and hasattr(field._table, '_primarykey')\ |
---|
3580 | and fieldname in field._table._primarykey: |
---|
3581 | # have to test this with multi-key tables |
---|
3582 | key = urlencode(dict([ |
---|
3583 | ((tablename in record |
---|
3584 | and isinstance(record, Row) |
---|
3585 | and isinstance(record[tablename], Row)) and |
---|
3586 | (k, record[tablename][k])) or (k, record[k]) |
---|
3587 | for k in field._table._primarykey])) |
---|
3588 | r = A(r, _href='%s/%s?%s' % (linkto, tablename, key)) |
---|
3589 | elif isinstance(field.type, str) and field.type.startswith('list:'): |
---|
3590 | r = represent(field, r or [], record) |
---|
3591 | elif field.represent: |
---|
3592 | r = represent(field, r, record) |
---|
3593 | elif field.type == 'blob' and r: |
---|
3594 | r = 'DATA' |
---|
3595 | elif field.type == 'upload': |
---|
3596 | if upload and r: |
---|
3597 | r = A(current.T('file'), _href='%s/%s' % (upload, r)) |
---|
3598 | elif r: |
---|
3599 | r = current.T('file') |
---|
3600 | else: |
---|
3601 | r = '' |
---|
3602 | elif field.type in ['string', 'text']: |
---|
3603 | r = str(field.formatter(r)) |
---|
3604 | truncate_by = truncate |
---|
3605 | if headers != {}: # new implement dict |
---|
3606 | if isinstance(headers[colname], dict): |
---|
3607 | if isinstance(headers[colname]['truncate'], int): |
---|
3608 | truncate_by = headers[colname]['truncate'] |
---|
3609 | if truncate_by is not None: |
---|
3610 | r = truncate_string(r, truncate_by) |
---|
3611 | attrcol = dict() # new implement dict |
---|
3612 | if headers != {}: |
---|
3613 | if isinstance(headers[colname], dict): |
---|
3614 | colclass = headers[colname]['class'] |
---|
3615 | if headers[colname]['selected']: |
---|
3616 | colclass = str(headers[colname] |
---|
3617 | ['class'] + " colselected").strip() |
---|
3618 | if colclass != "": |
---|
3619 | attrcol.update(_class=colclass) |
---|
3620 | |
---|
3621 | row.append(TD(r, **attrcol)) |
---|
3622 | |
---|
3623 | if extracolumns: # new implement dict |
---|
3624 | for c in extracolumns: |
---|
3625 | attrcol = dict() |
---|
3626 | colclass = c['class'] |
---|
3627 | if c['selected']: |
---|
3628 | colclass = str(c['class'] + " colselected").strip() |
---|
3629 | if colclass != "": |
---|
3630 | attrcol.update(_class=colclass) |
---|
3631 | contentfunc = c['content'] |
---|
3632 | row.append(TD(contentfunc(record, rc), **attrcol)) |
---|
3633 | |
---|
3634 | tbody.append(TR(_class=_class, *row)) |
---|
3635 | |
---|
3636 | if renderstyle: |
---|
3637 | components.append(STYLE(self.style())) |
---|
3638 | |
---|
3639 | components.append(TBODY(*tbody)) |
---|
3640 | |
---|
3641 | def style(self): |
---|
3642 | |
---|
3643 | css = """ |
---|
3644 | table tbody tr.w2p_odd { |
---|
3645 | background-color: #DFD; |
---|
3646 | } |
---|
3647 | table tbody tr.w2p_even { |
---|
3648 | background-color: #EFE; |
---|
3649 | } |
---|
3650 | table tbody tr.rowselected { |
---|
3651 | background-color: #FDD; |
---|
3652 | } |
---|
3653 | table tbody tr td.colselected { |
---|
3654 | background-color: #FDD; |
---|
3655 | } |
---|
3656 | table tbody tr:hover { |
---|
3657 | background: #DDF; |
---|
3658 | } |
---|
3659 | """ |
---|
3660 | |
---|
3661 | return css |
---|
3662 | |
---|
3663 | form_factory = SQLFORM.factory # for backward compatibility, deprecated |
---|
3664 | |
---|
3665 | |
---|
3666 | class ExportClass(object): |
---|
3667 | label = None |
---|
3668 | file_ext = None |
---|
3669 | content_type = None |
---|
3670 | |
---|
3671 | def __init__(self, rows): |
---|
3672 | self.rows = rows |
---|
3673 | |
---|
3674 | def represented(self): |
---|
3675 | def none_exception(value): |
---|
3676 | """ |
---|
3677 | Returns a cleaned up value that can be used for csv export: |
---|
3678 | |
---|
3679 | - unicode text is encoded as such |
---|
3680 | - None values are replaced with the given representation (default <NULL>) |
---|
3681 | """ |
---|
3682 | if value is None: |
---|
3683 | return '<NULL>' |
---|
3684 | elif isinstance(value, unicodeT): |
---|
3685 | return value.encode('utf8') |
---|
3686 | elif isinstance(value, Reference): |
---|
3687 | return int(value) |
---|
3688 | elif hasattr(value, 'isoformat'): |
---|
3689 | return value.isoformat()[:19].replace('T', ' ') |
---|
3690 | elif isinstance(value, (list, tuple)): # for type='list:..' |
---|
3691 | return bar_encode(value) |
---|
3692 | return value |
---|
3693 | |
---|
3694 | represented = [] |
---|
3695 | repr_cache = {} |
---|
3696 | for record in self.rows: |
---|
3697 | row = [] |
---|
3698 | for col in self.rows.colnames: |
---|
3699 | if not self.rows.db._adapter.REGEX_TABLE_DOT_FIELD.match(col): |
---|
3700 | row.append(record._extra[col]) |
---|
3701 | else: |
---|
3702 | # The grid code modifies rows.colnames, adding double quotes |
---|
3703 | # around the table and field names -- so they must be removed here. |
---|
3704 | (t, f) = [name.strip('"') for name in col.split('.')] |
---|
3705 | field = self.rows.db[t][f] |
---|
3706 | if isinstance(record.get(t, None), (Row, dict)): |
---|
3707 | value = record[t][f] |
---|
3708 | else: |
---|
3709 | value = record[f] |
---|
3710 | if field.type == 'blob' and value is not None: |
---|
3711 | value = '' |
---|
3712 | elif field.represent: |
---|
3713 | if field.type.startswith('reference'): |
---|
3714 | if field not in repr_cache: |
---|
3715 | repr_cache[field] = {} |
---|
3716 | if value not in repr_cache[field]: |
---|
3717 | repr_cache[field][value] = field.represent(value, record) |
---|
3718 | value = repr_cache[field][value] |
---|
3719 | else: |
---|
3720 | value = field.represent(value, record) |
---|
3721 | row.append(none_exception(value)) |
---|
3722 | |
---|
3723 | represented.append(row) |
---|
3724 | return represented |
---|
3725 | |
---|
3726 | def export(self): |
---|
3727 | raise NotImplementedError |
---|
3728 | |
---|
3729 | |
---|
3730 | class ExporterTSV(ExportClass): |
---|
3731 | # TSV, represent == True |
---|
3732 | label = 'TSV' |
---|
3733 | file_ext = "tsv" |
---|
3734 | content_type = "text/tab-separated-values" |
---|
3735 | |
---|
3736 | def __init__(self, rows): |
---|
3737 | ExportClass.__init__(self, rows) |
---|
3738 | |
---|
3739 | def export(self): # export TSV with field.represent |
---|
3740 | if self.rows: |
---|
3741 | s = StringIO() |
---|
3742 | self.rows.export_to_csv_file(s, represent=True,delimiter='\t',newline='\n') |
---|
3743 | return s.getvalue() |
---|
3744 | else: |
---|
3745 | return None |
---|
3746 | |
---|
3747 | |
---|
3748 | class ExporterTSV_hidden(ExportClass): |
---|
3749 | label = 'TSV' |
---|
3750 | file_ext = "tsv" |
---|
3751 | content_type = "text/tab-separated-values" |
---|
3752 | |
---|
3753 | def __init__(self, rows): |
---|
3754 | ExportClass.__init__(self, rows) |
---|
3755 | |
---|
3756 | def export(self): |
---|
3757 | if self.rows: |
---|
3758 | s = StringIO() |
---|
3759 | self.rows.export_to_csv_file(s,delimiter='\t',newline='\n') |
---|
3760 | return s.getvalue() |
---|
3761 | else: |
---|
3762 | return None |
---|
3763 | |
---|
3764 | |
---|
3765 | class ExporterCSV(ExportClass): |
---|
3766 | # CSV, represent == True |
---|
3767 | label = 'CSV' |
---|
3768 | file_ext = "csv" |
---|
3769 | content_type = "text/csv" |
---|
3770 | |
---|
3771 | def __init__(self, rows): |
---|
3772 | ExportClass.__init__(self, rows) |
---|
3773 | |
---|
3774 | def export(self): # export CSV with field.represent |
---|
3775 | if self.rows: |
---|
3776 | s = StringIO() |
---|
3777 | self.rows.export_to_csv_file(s, represent=True) |
---|
3778 | return s.getvalue() |
---|
3779 | else: |
---|
3780 | return None |
---|
3781 | |
---|
3782 | |
---|
3783 | class ExporterCSV_hidden(ExportClass): |
---|
3784 | # pure csv, no represent. |
---|
3785 | label = 'CSV' |
---|
3786 | file_ext = "csv" |
---|
3787 | content_type = "text/csv" |
---|
3788 | |
---|
3789 | def __init__(self, rows): |
---|
3790 | ExportClass.__init__(self, rows) |
---|
3791 | |
---|
3792 | def export(self): |
---|
3793 | if self.rows: |
---|
3794 | return self.rows.as_csv() |
---|
3795 | else: |
---|
3796 | return '' |
---|
3797 | |
---|
3798 | |
---|
3799 | class ExporterHTML(ExportClass): |
---|
3800 | label = 'HTML' |
---|
3801 | file_ext = "html" |
---|
3802 | content_type = "text/html" |
---|
3803 | |
---|
3804 | def __init__(self, rows): |
---|
3805 | ExportClass.__init__(self, rows) |
---|
3806 | |
---|
3807 | def export(self): |
---|
3808 | table = SQLTABLE(self.rows, truncate=None) if self.rows else '' |
---|
3809 | return '<html>\n<head>\n<meta http-equiv="content-type" content="text/html; charset=UTF-8" />\n</head>\n<body>\n%s\n</body>\n</html>' % (table or '') |
---|
3810 | |
---|
3811 | |
---|
3812 | class ExporterXML(ExportClass): |
---|
3813 | label = 'XML' |
---|
3814 | file_ext = "xml" |
---|
3815 | content_type = "text/xml" |
---|
3816 | |
---|
3817 | def __init__(self, rows): |
---|
3818 | ExportClass.__init__(self, rows) |
---|
3819 | |
---|
3820 | def export(self): |
---|
3821 | if self.rows: |
---|
3822 | return self.rows.as_xml() |
---|
3823 | else: |
---|
3824 | return '<rows></rows>' |
---|
3825 | |
---|
3826 | |
---|
3827 | class ExporterJSON(ExportClass): |
---|
3828 | label = 'JSON' |
---|
3829 | file_ext = "json" |
---|
3830 | content_type = "application/json" |
---|
3831 | |
---|
3832 | def __init__(self, rows): |
---|
3833 | ExportClass.__init__(self, rows) |
---|
3834 | |
---|
3835 | def export(self): |
---|
3836 | if self.rows: |
---|
3837 | return self.rows.as_json() |
---|
3838 | else: |
---|
3839 | return 'null' |
---|