source: tailbone/tailbone/forms/core.py @ 0f5d668

Last change on this file since 0f5d668 was 0f5d668, checked in by Lance Edgar <ledgar@…>, 6 months ago

Add "plain" date widget

to avoid deform.addCallback() JS for mobile forms

surely there's a better solution, but this works for now...

  • Property mode set to 100644
File size: 32.9 KB
Line 
1# -*- coding: utf-8; -*-
2################################################################################
3#
4#  Rattail -- Retail Software Framework
5#  Copyright © 2010-2018 Lance Edgar
6#
7#  This file is part of Rattail.
8#
9#  Rattail is free software: you can redistribute it and/or modify it under the
10#  terms of the GNU General Public License as published by the Free Software
11#  Foundation, either version 3 of the License, or (at your option) any later
12#  version.
13#
14#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
15#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17#  details.
18#
19#  You should have received a copy of the GNU General Public License along with
20#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Forms Core
25"""
26
27from __future__ import unicode_literals, absolute_import
28
29import datetime
30import logging
31
32import six
33import sqlalchemy as sa
34from sqlalchemy import orm
35from sqlalchemy.ext.associationproxy import AssociationProxy, ASSOCIATION_PROXY
36
37from rattail.time import localtime
38from rattail.util import prettify, pretty_boolean, pretty_hours, pretty_quantity
39
40import colander
41import deform
42from colanderalchemy import SQLAlchemySchemaNode
43from colanderalchemy.schema import _creation_order
44from deform import widget as dfwidget
45from pyramid_deform import SessionFileUploadTempStore
46from pyramid.renderers import render
47from webhelpers2.html import tags, HTML
48
49from tailbone.util import raw_datetime
50from . import types
51from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
52
53
54log = logging.getLogger(__name__)
55
56
57def get_association_proxy(mapper, field):
58    """
59    Returns the association proxy corresponding to the given field name if one
60    exists, or ``None``.
61    """
62    try:
63        desc = getattr(mapper.all_orm_descriptors, field)
64    except AttributeError:
65        pass
66    else:
67        if desc.extension_type == ASSOCIATION_PROXY:
68            return desc
69
70
71def get_association_proxy_target(inspector, field):
72    """
73    Returns the property on the main class, which represents the "target"
74    for the given association proxy field name.  Typically this will refer
75    to the "extension" model class.
76    """
77    proxy = get_association_proxy(inspector, field)
78    if proxy:
79        proxy_target = inspector.get_property(proxy.target_collection)
80        if isinstance(proxy_target, orm.RelationshipProperty) and not proxy_target.uselist:
81            return proxy_target
82
83
84def get_association_proxy_column(inspector, field):
85    """
86    Returns the property on the proxy target class, for the column which is
87    reflected by the proxy.
88    """
89    proxy_target = get_association_proxy_target(inspector, field)
90    if proxy_target:
91        if proxy_target.mapper.has_property(field):
92            prop = proxy_target.mapper.get_property(field)
93            if isinstance(prop, orm.ColumnProperty) and isinstance(prop.columns[0], sa.Column):
94                return prop
95
96
97class CustomSchemaNode(SQLAlchemySchemaNode):
98
99    def association_proxy(self, field):
100        """
101        Returns the association proxy corresponding to the given field name if
102        one exists, or ``None``.
103        """
104        return get_association_proxy(self.inspector, field)
105
106    def association_proxy_target(self, field):
107        """
108        Returns the property on the main class, which represents the "target"
109        for the given association proxy field name.  Typically this will refer
110        to the "extension" model class.
111        """
112        proxy = self.association_proxy(field)
113        if proxy:
114            proxy_target = self.inspector.get_property(proxy.target_collection)
115            if isinstance(proxy_target, orm.RelationshipProperty) and not proxy_target.uselist:
116                return proxy_target
117
118    def association_proxy_column(self, field):
119        """
120        Returns the property on the proxy target class, for the column which is
121        reflected by the proxy.
122        """
123        proxy_target = self.association_proxy_target(field)
124        if proxy_target:
125            prop = proxy_target.mapper.get_property(field)
126            if isinstance(prop, orm.ColumnProperty) and isinstance(prop.columns[0], sa.Column):
127                return prop
128
129    def supported_association_proxy(self, field):
130        """
131        Returns boolean indicating whether the association proxy corresponding
132        to the given field name, is "supported" with typical logic.
133        """
134        if not self.association_proxy_column(field):
135            return False
136        return True
137
138    def add_nodes(self, includes, excludes, overrides):
139        """
140        Add all automatic nodes to the schema.
141
142        .. note::
143           This method was copied from upstream and modified to add automatic
144           handling of "association proxy" fields.
145        """
146        if set(excludes) & set(includes):
147            msg = 'excludes and includes are mutually exclusive.'
148            raise ValueError(msg)
149
150        # sorted to maintain the order in which the attributes
151        # are defined
152        properties = sorted(self.inspector.attrs, key=_creation_order)
153        if excludes:
154            if includes:
155                raise ValueError("Must pass includes *or* excludes, but not both")
156            supported = [prop.key for prop in properties
157                         if prop.key not in excludes]
158        elif includes:
159            supported = includes
160        elif includes is not None:
161            supported = []
162
163        for name in supported:
164            prop = self.inspector.attrs.get(name, name)
165
166            if name in excludes or (includes and name not in includes):
167                log.debug('Attribute %s skipped imperatively', name)
168                continue
169
170            name_overrides_copy = overrides.get(name, {}).copy()
171
172            if (isinstance(prop, orm.ColumnProperty)
173                    and isinstance(prop.columns[0], sa.Column)):
174                node = self.get_schema_from_column(
175                    prop,
176                    name_overrides_copy
177                )
178            elif isinstance(prop, orm.RelationshipProperty):
179                if prop.mapper.class_ in self.parents_ and name not in includes:
180                    continue
181                node = self.get_schema_from_relationship(
182                    prop,
183                    name_overrides_copy
184                )
185            elif isinstance(prop, colander.SchemaNode):
186                node = prop
187            else:
188
189                # magic for association proxy fields
190                column = self.association_proxy_column(name)
191                if column:
192                    node = self.get_schema_from_column(column, name_overrides_copy)
193
194                else:
195                    log.debug(
196                        'Attribute %s skipped due to not being '
197                        'a ColumnProperty or RelationshipProperty',
198                        name
199                    )
200                    continue
201
202            if node is not None:
203                self.add(node)
204
205    def get_schema_from_relationship(self, prop, overrides):
206        """ Build and return a :class:`colander.SchemaNode` for a relationship.
207        """
208
209        # for some reason ColanderAlchemy wants to crawl our entire ORM by
210        # default, by way of relationships.  this 'excludes' hack is used to
211        # prevent that, by forcing skip of 2nd-level relationships
212
213        excludes = []
214        if isinstance(prop, orm.RelationshipProperty):
215            for next_prop in prop.mapper.iterate_properties:
216
217                # don't include secondary relationships
218                if isinstance(next_prop, orm.RelationshipProperty):
219                    excludes.append(next_prop.key)
220
221                # don't include fields of binary type
222                elif isinstance(next_prop, orm.ColumnProperty):
223                    for column in next_prop.columns:
224                        if isinstance(column.type, sa.LargeBinary):
225                            excludes.append(next_prop.key)
226
227        if excludes:
228            overrides['excludes'] = excludes
229
230        return super(CustomSchemaNode, self).get_schema_from_relationship(prop, overrides)
231
232    def dictify(self, obj):
233        """ Return a dictified version of `obj` using schema information.
234
235        .. note::
236           This method was copied from upstream and modified to add automatic
237           handling of "association proxy" fields.
238        """
239        dict_ = super(CustomSchemaNode, self).dictify(obj)
240        for node in self:
241
242            name = node.name
243            if name not in dict_:
244                # we're only processing association proxy fields here
245                if not self.supported_association_proxy(name):
246                    continue
247
248                value = getattr(obj, name)
249                if value is None:
250                    if isinstance(node.typ, colander.String):
251                        # colander has an issue with `None` on a String type
252                        #  where it translates it into "None".  Let's check
253                        #  for that specific case and turn it into a
254                        #  `colander.null`.
255                        dict_[name] = colander.null
256                    else:
257                        # A specific case this helps is with Integer where
258                        #  `None` is an invalid value.  We call serialize()
259                        #  to test if we have a value that will work later
260                        #  for serialization and then allow it if it doesn't
261                        #  raise an exception.  Hopefully this also catches
262                        #  issues with user defined types and future issues.
263                        try:
264                            node.serialize(value)
265                        except:
266                            dict_[name] = colander.null
267                        else:
268                            dict_[name] = value
269                else:
270                    dict_[name] = value
271
272        return dict_
273
274    def objectify(self, dict_, context=None):
275        """ Return an object representing ``dict_`` using schema information.
276
277        .. note::
278           This method was copied from upstream and modified to add automatic
279           handling of "association proxy" fields.
280        """
281        mapper = self.inspector
282        context = mapper.class_() if context is None else context
283        for attr in dict_:
284            if mapper.has_property(attr):
285                prop = mapper.get_property(attr)
286                if hasattr(prop, 'mapper'):
287                    cls = prop.mapper.class_
288                    if prop.uselist:
289                        # Sequence of objects
290                        value = [self[attr].children[0].objectify(obj)
291                                 for obj in dict_[attr]]
292                    else:
293                        # Single object
294                        value = self[attr].objectify(dict_[attr])
295                else:
296                     value = dict_[attr]
297                     if value is colander.null:
298                         # `colander.null` is never an appropriate
299                         #  value to be placed on an SQLAlchemy object
300                         #  so we translate it into `None`.
301                         value = None
302                setattr(context, attr, value)
303
304            else:
305
306                # try to process association proxy field
307                if self.supported_association_proxy(attr):
308                    value = dict_[attr]
309                    if value is colander.null:
310                        # `colander.null` is never an appropriate
311                        #  value to be placed on an SQLAlchemy object
312                        #  so we translate it into `None`.
313                        value = None
314                    setattr(context, attr, value)
315
316                else:
317                    # Ignore attributes if they are not mapped
318                    log.debug(
319                        'SQLAlchemySchemaNode.objectify: %s not found on '
320                        '%s. This property has been ignored.',
321                        attr, self
322                    )
323                    continue
324
325        return context
326
327
328class Form(object):
329    """
330    Base class for all forms.
331    """
332    save_label = "Save"
333    update_label = "Save"
334    show_cancel = True
335    auto_disable = True
336    auto_disable_save = True
337    auto_disable_cancel = True
338
339    def __init__(self, fields=None, schema=None, request=None, mobile=False, readonly=False, readonly_fields=[],
340                 model_instance=None, model_class=None, nodes={}, enums={}, labels={}, renderers=None,
341                 hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
342                 action_url=None, cancel_url=None):
343
344        self.fields = None
345        if fields is not None:
346            self.set_fields(fields)
347        self.schema = schema
348        if self.fields is None and self.schema:
349            self.set_fields([f.name for f in self.schema])
350        self.request = request
351        self.mobile = mobile
352        self.readonly = readonly
353        self.readonly_fields = set(readonly_fields or [])
354        self.model_instance = model_instance
355        self.model_class = model_class
356        if self.model_instance and not self.model_class and not isinstance(self.model_instance, dict):
357            self.model_class = type(self.model_instance)
358        if self.model_class and self.fields is None:
359            self.set_fields(self.make_fields())
360        self.nodes = nodes or {}
361        self.enums = enums or {}
362        self.labels = labels or {}
363        if renderers is None and self.model_class:
364            self.renderers = self.make_renderers()
365        else:
366            self.renderers = renderers or {}
367        self.hidden = hidden or {}
368        self.widgets = widgets or {}
369        self.defaults = defaults or {}
370        self.validators = validators or {}
371        self.required = required or {}
372        self.helptext = helptext or {}
373        self.focus_spec = focus_spec
374        self.action_url = action_url
375        self.cancel_url = cancel_url
376
377    def __contains__(self, item):
378        return item in self.fields
379
380    def set_fields(self, fields):
381        self.fields = FieldList(fields)
382
383    def make_fields(self):
384        """
385        Return a default list of fields, based on :attr:`model_class`.
386        """
387        if not self.model_class:
388            raise ValueError("Must define model_class to use make_fields()")
389
390        mapper = orm.class_mapper(self.model_class)
391
392        # first add primary column fields
393        fields = FieldList([prop.key for prop in mapper.iterate_properties
394                            if not prop.key.startswith('_')
395                            and prop.key != 'versions'])
396
397        # then add association proxy fields
398        for key, desc in sa.inspect(self.model_class).all_orm_descriptors.items():
399            if desc.extension_type == ASSOCIATION_PROXY:
400                fields.append(key)
401
402        return fields
403
404    def make_renderers(self):
405        """
406        Return a default set of field renderers, based on :attr:`model_class`.
407        """
408        if not self.model_class:
409            raise ValueError("Must define model_class to use make_renderers()")
410
411        inspector = sa.inspect(self.model_class)
412        renderers = {}
413
414        # TODO: clearly this should be leaner...
415
416        # first look at regular column fields
417        for prop in inspector.iterate_properties:
418            if isinstance(prop, orm.ColumnProperty):
419                if len(prop.columns) == 1:
420                    column = prop.columns[0]
421                    if isinstance(column.type, sa.DateTime):
422                        renderers[prop.key] = self.render_datetime
423                    elif isinstance(column.type, sa.Boolean):
424                        renderers[prop.key] = self.render_boolean
425
426        # then look at association proxy fields
427        for key, desc in inspector.all_orm_descriptors.items():
428            if desc.extension_type == ASSOCIATION_PROXY:
429                prop = get_association_proxy_column(inspector, key)
430                if prop:
431                    column = prop.columns[0]
432                    if isinstance(column.type, sa.DateTime):
433                        renderers[key] = self.render_datetime
434                    elif isinstance(column.type, sa.Boolean):
435                        renderers[key] = self.render_boolean
436
437        return renderers
438
439    def append(self, field):
440        self.fields.append(field)
441
442    def insert_before(self, field, newfield):
443        self.fields.insert_before(field, newfield)
444
445    def insert_after(self, field, newfield):
446        self.fields.insert_after(field, newfield)
447
448    def replace(self, field, newfield):
449        self.insert_after(field, newfield)
450        self.remove(field)
451
452    def remove(self, *args):
453        for arg in args:
454            if arg in self.fields:
455                self.fields.remove(arg)
456
457    # TODO: deprecare / remove this
458    def remove_field(self, key):
459        self.remove(key)
460
461    # TODO: deprecare / remove this
462    def remove_fields(self, *args):
463        self.remove(*args)
464
465    def make_schema(self):
466        if not self.schema:
467
468            if not self.model_class:
469                # TODO
470                raise NotImplementedError
471
472            mapper = orm.class_mapper(self.model_class)
473
474            # first filter our "full" field list so we ignore certain ones.  in
475            # particular we don't want readonly fields in the schema, or any
476            # which appear to be "private"
477            includes = [f for f in self.fields
478                        if f not in self.readonly_fields
479                        and not f.startswith('_')
480                        and f != 'versions']
481
482            # derive list of "auto included" fields.  this is all "included"
483            # fields which are part of the SQLAlchemy ORM for the object
484            auto_includes = []
485            property_keys = [p.key for p in mapper.iterate_properties]
486            inspector = sa.inspect(self.model_class)
487            for field in includes:
488                if field in self.nodes:
489                    continue    # these are explicitly set; no magic wanted
490                if field in property_keys:
491                    auto_includes.append(field)
492                elif get_association_proxy(inspector, field):
493                    auto_includes.append(field)
494
495            # make schema - only include *property* fields at this point
496            schema = CustomSchemaNode(self.model_class, includes=auto_includes)
497
498            # for now, must manually add any "extra" fields?  this includes all
499            # association proxy fields, not sure how other fields will behave
500            for field in includes:
501                if field not in schema:
502                    node = self.nodes.get(field)
503                    if not node:
504                        node = colander.SchemaNode(colander.String(), name=field, missing='')
505                    if not node.name:
506                        node.name = field
507                    schema.add(node)
508
509            # apply any label overrides
510            for key, label in self.labels.items():
511                if key in schema:
512                    schema[key].title = label
513
514            # apply any widget overrides
515            for key, widget in self.widgets.items():
516                if key in schema:
517                    schema[key].widget = widget
518
519            # TODO: we are now doing this when making deform.Form, in which
520            # case, do we still need to do it here?
521            # apply any default values
522            for key, default in self.defaults.items():
523                if key in schema:
524                    schema[key].default = default
525
526            # apply any validators
527            for key, validator in self.validators.items():
528                if key in schema:
529                    schema[key].validator = validator
530
531            # apply required flags
532            for key, required in self.required.items():
533                if key in schema:
534                    if required:
535                        schema[key].missing = colander.required
536                    else:
537                        schema[key].missing = None # TODO?
538
539            self.schema = schema
540
541        return self.schema
542
543    def set_label(self, key, label):
544        self.labels[key] = label
545
546        # update schema if necessary
547        if self.schema and key in self.schema:
548            self.schema[key].title = label
549
550    def get_label(self, key):
551        return self.labels.get(key, prettify(key))
552
553    def set_readonly(self, key, readonly=True):
554        if readonly:
555            self.readonly_fields.add(key)
556        else:
557            if key in self.readonly_fields:
558                self.readonly_fields.remove(key)
559
560    def set_node(self, key, nodeinfo, **kwargs):
561        if isinstance(nodeinfo, colander.SchemaNode):
562            node = nodeinfo
563        else:
564            kwargs.setdefault('name', key)
565            node = colander.SchemaNode(nodeinfo, **kwargs)
566        self.nodes[key] = node
567
568    def set_type(self, key, type_, **kwargs):
569        if type_ == 'datetime':
570            self.set_renderer(key, self.render_datetime)
571        elif type_ == 'datetime_local':
572            self.set_renderer(key, self.render_datetime_local)
573        elif type_ == 'date_plain':
574            self.set_widget(key, PlainDateWidget())
575        elif type_ == 'date_jquery':
576            # TODO: is this safe / a good idea?
577            # self.set_node(key, colander.Date())
578            self.set_widget(key, JQueryDateWidget())
579        elif type_ == 'time_jquery':
580            self.set_node(key, types.JQueryTime())
581            self.set_widget(key, JQueryTimeWidget())
582        elif type_ == 'duration':
583            self.set_renderer(key, self.render_duration)
584        elif type_ == 'boolean':
585            self.set_renderer(key, self.render_boolean)
586            self.set_widget(key, dfwidget.CheckboxWidget())
587        elif type_ == 'currency':
588            self.set_renderer(key, self.render_currency)
589        elif type_ == 'quantity':
590            self.set_renderer(key, self.render_quantity)
591        elif type_ == 'percent':
592            self.set_renderer(key, self.render_percent)
593        elif type_ == 'gpc':
594            self.set_renderer(key, self.render_gpc)
595        elif type_ == 'enum':
596            self.set_renderer(key, self.render_enum)
597        elif type_ == 'codeblock':
598            self.set_renderer(key, self.render_codeblock)
599            self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
600        elif type_ == 'text':
601            self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
602        elif type_ == 'file':
603            tmpstore = SessionFileUploadTempStore(self.request)
604            kw = {'widget': dfwidget.FileUploadWidget(tmpstore),
605                  'title': self.get_label(key)}
606            if 'required' in kwargs and not kwargs['required']:
607                kw['missing'] = colander.null
608            self.set_node(key, colander.SchemaNode(deform.FileData(), **kw))
609        else:
610            raise ValueError("unknown type for '{}' field: {}".format(key, type_))
611
612    def set_enum(self, key, enum, empty=None):
613        if enum:
614            self.enums[key] = enum
615            self.set_type(key, 'enum')
616            values = list(enum.items())
617            if empty:
618                values.insert(0, empty)
619            self.set_widget(key, dfwidget.SelectWidget(values=values))
620        else:
621            self.enums.pop(key, None)
622
623    def get_enum(self, key):
624        return self.enums.get(key)
625
626    # TODO: i don't think this is actually being used anywhere..?
627    def set_enum_value(self, key, enum_key, enum_value):
628        enum = self.enums.get(key)
629        if enum:
630            enum[enum_key] = enum_value
631
632    def set_renderer(self, key, renderer):
633        if renderer is None:
634            if key in self.renderers:
635                del self.renderers[key]
636        else:
637            self.renderers[key] = renderer
638
639    def set_hidden(self, key, hidden=True):
640        self.hidden[key] = hidden
641
642    def set_widget(self, key, widget):
643        self.widgets[key] = widget
644
645        # update schema if necessary
646        if self.schema and key in self.schema:
647            self.schema[key].widget = widget
648
649    def set_validator(self, key, validator):
650        self.validators[key] = validator
651
652    def set_required(self, key, required=True):
653        """
654        Set whether or not value is required for a given field.
655        """
656        self.required[key] = required
657
658    def set_default(self, key, value):
659        """
660        Set the default value for a given field.
661        """
662        self.defaults[key] = value
663
664    def set_helptext(self, key, value):
665        """
666        Set the help text for a given field.
667        """
668        self.helptext[key] = value
669
670    def has_helptext(self, key):
671        """
672        Returns boolean indicating whether the given field has accompanying
673        help text.
674        """
675        return key in self.helptext
676
677    def render_helptext(self, key):
678        """
679        Render the help text for the given field.
680        """
681        return self.helptext[key]
682
683    def render(self, template=None, **kwargs):
684        if not template:
685            if self.readonly:
686                template = '/forms/form_readonly.mako'
687            else:
688                template = '/forms/form.mako'
689        context = kwargs
690        context['form'] = self
691        return render(template, context)
692
693    def make_deform_form(self):
694        if not hasattr(self, 'deform_form'):
695
696            schema = self.make_schema()
697
698            # TODO: we are still also doing this when making the schema, but
699            # seems like this should be the right place instead?
700            # apply any default values
701            for key, default in self.defaults.items():
702                if key in schema:
703                    schema[key].default = default
704
705            # get initial form values from model instance
706            kwargs = {}
707            if self.model_instance:
708                if self.model_class:
709                    kwargs['appstruct'] = schema.dictify(self.model_instance)
710                else:
711                    kwargs['appstruct'] = self.model_instance
712
713            # create form
714            form = deform.Form(schema, **kwargs)
715            form.tailbone_form = self
716
717            # set readonly widget where applicable
718            for field in self.readonly_fields:
719                if field in form:
720                    form[field].widget = ReadonlyWidget()
721
722            self.deform_form = form
723
724        return self.deform_form
725
726    def render_deform(self, dform=None, template='/forms/deform.mako', **kwargs):
727        if dform is None:
728            dform = self.make_deform_form()
729
730        # TODO: would perhaps be nice to leverage deform's default rendering
731        # someday..? i.e. using Chameleon *.pt templates
732        # return form.render()
733
734        context = kwargs
735        context['form'] = self
736        context['dform'] = dform
737        context.setdefault('form_kwargs', {})
738        # TODO: deprecate / remove the latter option here
739        if self.auto_disable_save or self.auto_disable:
740            context['form_kwargs']['class_'] = 'autodisable'
741        if self.focus_spec:
742            context['form_kwargs']['data-focus'] = self.focus_spec
743        context['request'] = self.request
744        context['readonly_fields'] = self.readonly_fields
745        context['render_field_readonly'] = self.render_field_readonly
746        return render('/forms/deform.mako', context)
747
748    def field_visible(self, field):
749        if self.hidden and self.hidden.get(field):
750            return False
751        return True
752
753    def render_field_readonly(self, field_name, **kwargs):
754        """
755        Render the given field completely, but in read-only fashion.
756
757        Note that this method will generate the wrapper div and label, as well
758        as the field value.
759        """
760        if field_name not in self.fields:
761            return ''
762
763        # TODO: fair bit of duplication here, should merge with deform.mako
764        label = HTML.tag('label', self.get_label(field_name), for_=field_name)
765        field = self.render_field_value(field_name) or ''
766        field_div = HTML.tag('div', class_='field', c=[field])
767        contents = [label, field_div]
768
769        if self.has_helptext(field_name):
770            contents.append(HTML.tag('span', class_='instructions',
771                                     c=[self.render_helptext(field_name)]))
772
773        return HTML.tag('div', class_='field-wrapper {}'.format(field_name), c=contents)
774
775    def render_field_value(self, field_name):
776        record = self.model_instance
777        if self.renderers and field_name in self.renderers:
778            return self.renderers[field_name](record, field_name)
779        return self.render_generic(record, field_name)
780
781    def render_generic(self, record, field_name):
782        value = self.obtain_value(record, field_name)
783        if value is None:
784            return ""
785        return six.text_type(value)
786
787    def render_datetime(self, record, field_name):
788        value = self.obtain_value(record, field_name)
789        if value is None:
790            return ""
791        return raw_datetime(self.request.rattail_config, value)
792
793    def render_datetime_local(self, record, field_name):
794        value = self.obtain_value(record, field_name)
795        if value is None:
796            return ""
797        value = localtime(self.request.rattail_config, value)
798        return raw_datetime(self.request.rattail_config, value)
799
800    def render_duration(self, record, field_name):
801        value = self.obtain_value(record, field_name)
802        if value is None:
803            return ""
804        return pretty_hours(datetime.timedelta(seconds=value))
805
806    def render_boolean(self, record, field_name):
807        value = self.obtain_value(record, field_name)
808        return pretty_boolean(value)
809
810    def render_currency(self, record, field_name):
811        value = self.obtain_value(record, field_name)
812        if value is None:
813            return ""
814        try:
815            if value < 0:
816                return "(${:0,.2f})".format(0 - value)
817            return "${:0,.2f}".format(value)
818        except ValueError:
819            return six.text_type(value)
820
821    def render_quantity(self, obj, field):
822        value = self.obtain_value(obj, field)
823        if value is None:
824            return ""
825        return pretty_quantity(value)
826
827    def render_percent(self, obj, field):
828        value = self.obtain_value(obj, field)
829        if value is None:
830            return ""
831        return "{:0.3f} %".format(value * 100)
832
833    def render_gpc(self, obj, field):
834        value = self.obtain_value(obj, field)
835        if value is None:
836            return ""
837        return value.pretty()
838
839    def render_enum(self, record, field_name):
840        value = self.obtain_value(record, field_name)
841        if value is None:
842            return ""
843        enum = self.enums.get(field_name)
844        if enum and value in enum:
845            return six.text_type(enum[value])
846        return six.text_type(value)
847
848    def render_codeblock(self, record, field_name):
849        value = self.obtain_value(record, field_name)
850        if value is None:
851            return ""
852        return HTML.tag('pre', value)
853
854    def obtain_value(self, record, field_name):
855        if record:
856            try:
857                return record[field_name]
858            except TypeError:
859                return getattr(record, field_name, None)
860
861        # TODO: is this always safe to do?
862        elif self.defaults and field_name in self.defaults:
863            return self.defaults[field_name]
864
865    def validate(self, *args, **kwargs):
866        if kwargs.pop('newstyle', False):
867            # yay, new behavior!
868            if hasattr(self, 'validated'):
869                del self.validated
870            if self.request.method != 'POST':
871                return False
872            controls = self.request.POST.items()
873            dform = self.make_deform_form()
874            try:
875                self.validated = dform.validate(controls)
876                return True
877            except deform.ValidationFailure:
878                return False
879
880        else: # legacy behavior
881            raise_error = kwargs.pop('raise_error', True)
882            dform = self.make_deform_form()
883            try:
884                return dform.validate(*args, **kwargs)
885            except deform.ValidationFailure:
886                if raise_error:
887                    raise
888
889
890class FieldList(list):
891    """
892    Convenience wrapper for a form's field list.
893    """
894
895    def insert_before(self, field, newfield):
896        i = self.index(field)
897        self.insert(i, newfield)
898
899    def insert_after(self, field, newfield):
900        i = self.index(field)
901        self.insert(i + 1, newfield)
902
903
904@colander.deferred
905def upload_widget(node, kw):
906    request = kw['request']
907    tmpstore = SessionFileUploadTempStore(request)
908    return dfwidget.FileUploadWidget(tmpstore)
909
910
911class SimpleFileImport(colander.Schema):
912    """
913    Schema for simple file import.  Note that you must bind your ``request``
914    object to this schema, i.e.::
915
916       schema = SimpleFileImport().bind(request=request)
917    """
918    filename = colander.SchemaNode(deform.FileData(),
919                                   widget=upload_widget)
Note: See TracBrowser for help on using the repository browser.