source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/sqlhtml.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 155.0 KB
Line 
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
9Holds:
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
17import datetime
18import re
19import copy
20
21import os
22from gluon._compat import StringIO,unichr, urllib_quote, iteritems, basestring, long, integer_types, unicodeT, to_native, to_unicode, urlencode
23from gluon.http import HTTP, redirect
24from gluon.html import XmlComponent, truncate_string
25from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG
26from gluon.html import FORM, INPUT, LABEL, OPTION, SELECT, COL, COLGROUP
27from gluon.html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE, SCRIPT
28from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY
29from pydal.base import DEFAULT
30from pydal.objects import Table, Row, Expression, Field, Set, Rows
31from pydal.adapters.base import CALLABLETYPES
32from pydal.helpers.methods import smart_query, bar_encode, _repr_ref, merge_tablemaps
33from pydal.helpers.classes import Reference, SQLCustomType
34from pydal.default_validators import default_validators
35from gluon.storage import Storage
36from gluon.utils import md5_hash
37from gluon.validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE
38from gluon.validators import IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE
39from gluon.validators import IS_STRONG
40
41import gluon.serializers as serializers
42from gluon.globals import current
43from functools import reduce
44
45try:
46    import gluon.settings as settings
47except ImportError:
48    settings = {}
49
50
51REGEX_WIDGET_CLASS = re.compile(r'^\w*')
52
53
54def add_class(a, b):
55    return a + ' ' + b if a else b
56
57def 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
73def 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
85class 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
107def safe_int(x, i=0):
108    try:
109        return int(x)
110    except (ValueError, TypeError):
111        return i
112
113
114def safe_float(x):
115    try:
116        return float(x)
117    except (ValueError, TypeError):
118        return 0
119
120
121def 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
145PLURALIZE_RULES = None
146
147def 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
173class 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
227class 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
247class IntegerWidget(StringWidget):
248    _class = 'integer'
249
250
251class DoubleWidget(StringWidget):
252    _class = 'double'
253
254
255class DecimalWidget(StringWidget):
256    _class = 'decimal'
257
258
259class TimeWidget(StringWidget):
260    _class = 'time'
261
262
263class DateWidget(StringWidget):
264    _class = 'date'
265
266
267class DatetimeWidget(StringWidget):
268    _class = 'datetime'
269
270
271class 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
287class 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
305class 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
322class 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
361class 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
386class 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
405class 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
473class 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
547class 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
578class 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
688class 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
927def 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
938def 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
950def 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
961def 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
971def 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
982def 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
1027def 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
1074def 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
1127def 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
1176def 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
1228class 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('&nbsp;')  # 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('&#x25B2;'), XML('&#x25BC;')),
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
3385class 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
3663form_factory = SQLFORM.factory  # for backward compatibility, deprecated
3664
3665
3666class 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
3730class 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
3748class 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
3765class 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
3783class 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
3799class 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
3812class 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
3827class 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'
Note: See TracBrowser for help on using the repository browser.