source: main/waeup.ikoba/trunk/src/waeup/ikoba/utils/converters.py @ 12281

Last change on this file since 12281 was 12261, checked in by Henrik Bettermann, 10 years ago

Modify getWidgetsData so that we always get the same error format. Should also be changed in Kofa.

  • Property svn:keywords set to Id
File size: 11.7 KB
RevLine 
[7196]1## $Id: converters.py 12261 2014-12-19 08:14:25Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
[4805]18"""Converters for zope.schema-based datatypes.
19"""
20import grok
[6263]21from zope.component import createObject
[6258]22from zope.formlib import form
[6278]23from zope.formlib.boolwidgets import CheckBoxWidget
[6260]24from zope.formlib.form import (
[12261]25    _widgetKey, WidgetInputError, ValidationError, InputErrors,
26    expandPrefix)
27from zope.formlib.interfaces import IInputWidget, ConversionError
[6276]28from zope.interface import Interface
[6258]29from zope.publisher.browser import TestRequest
[7932]30from zope.schema.interfaces import IList
[11949]31from waeup.ikoba.interfaces import (
[7932]32    IObjectConverter, IResultEntryField, IFieldConverter, SubjectSource,
[8214]33    GradeSource, DELETION_MARKER, IGNORE_MARKER)
[11949]34from waeup.ikoba.schema.interfaces import IPhoneNumber
35from waeup.ikoba.schoolgrades import ResultEntry
[6258]36
[6278]37class ExtendedCheckBoxWidget(CheckBoxWidget):
38    """A checkbox widget that supports more input values as True/False
[7597]39    markers.
[6278]40
41    The default bool widget expects the string 'on' as only valid
42    ``True`` value in HTML forms for bool fields.
43
44    This widget also accepts '1', 'true' and 'yes' for that. Also all
45    uppercase/lowecase combinations of these strings are accepted.
46
47    The widget still renders ``True`` to ``'on'`` when a form is
48    generated.
49    """
50    true_markers = ['1', 'true', 'on', 'yes']
51
52    def _toFieldValue(self, input):
53        """Convert from HTML presentation to Python bool."""
54        if not isinstance(input, basestring):
55            return False
56        return input.lower() in self.true_markers
57
58    def _getFormInput(self):
59        """Returns the form input used by `_toFieldValue`.
60
61        Return values:
62
63          ``'on'``  checkbox is checked
64          ``''``    checkbox is not checked
65          ``None``  form input was not provided
66
67        """
68        value = self.request.get(self.name)
69        if isinstance(value, basestring):
70            value = value.lower()
71        if value in self.true_markers:
72            return 'on'
73        elif self.name + '.used' in self.request:
74            return ''
75        else:
76            return None
77
[6260]78def getWidgetsData(widgets, form_prefix, data):
[6273]79    """Get data and validation errors from `widgets` for `data`.
80
81    Updates the dict in `data` with values from the widgets in
82    `widgets`.
83
84    Returns a list of tuples ``(<WIDGET_NAME>, <ERROR>)`` where
85    ``<WIDGET_NAME>`` is a widget name (normally the same as the
86    associated field name) and ``<ERROR>`` is the exception that
87    happened for that widget/field.
88
89    This is merely a copy from the same-named function in
[12261]90    :mod:`zope.formlib.form`. The first difference is that we also
[6273]91    store the fieldname for which a validation error happened in the
92    returned error list (what the original does not do).
[12261]93    The second difference is that we return only ConversionError objects.
[6273]94    """
[6260]95    errors = []
96    form_prefix = expandPrefix(form_prefix)
97
98    for input, widget in widgets.__iter_input_and_widget__():
99        if input and IInputWidget.providedBy(widget):
100            name = _widgetKey(widget, form_prefix)
101
102            if not widget.hasInput():
103                continue
104
105            try:
106                data[name] = widget.getInputValue()
107            except ValidationError, error:
[12261]108                error = ConversionError(u'Validation failed')
[6260]109                errors.append((name, error))
[12261]110            except WidgetInputError, error:
111                error = ConversionError(u'Invalid input')
[6260]112                errors.append((name, error))
[12261]113            except ConversionError, error:
114                errors.append((name, error))
[6260]115
116    return errors
117
[7932]118class DefaultFieldConverter(grok.Adapter):
119    grok.context(Interface)
[8214]120    grok.implements(IFieldConverter)
[6278]121
[8214]122    def request_data(self, name, value, schema_field, prefix='',
123                     mode='create'):
[9932]124        if prefix == 'form.sex' and isinstance(value, basestring):
125            value = value.lower()
[7932]126        return {prefix: value}
127
128class ListFieldConverter(grok.Adapter):
129    grok.context(IList)
[8214]130    grok.implements(IFieldConverter)
[7932]131
[8214]132    def request_data(self, name, value, schema_field, prefix='',
133                     mode='create'):
[7932]134        value_type = schema_field.value_type
135        try:
136            items = eval(value)
137        except:
138            return {prefix: value}
139        result = {'%s.count' % prefix: len(items)}
140        for num, item in enumerate(items):
141            sub_converter = IFieldConverter(value_type)
142            result.update(sub_converter.request_data(
143                unicode(num), unicode(item),
144                value_type, "%s.%s." % (prefix, num)))
145        return result
146
[8175]147class PhoneNumberFieldConverter(grok.Adapter):
148    """Convert strings into dict as expected from forms feeding PhoneWidget.
149
150    If you want strings without extra-checks imported, you can use
151    schema.TextLine in your interface instead of PhoneNumber.
152    """
153    grok.context(IPhoneNumber)
[8214]154    grok.implements(IFieldConverter)
[8175]155
[8214]156    def request_data(self, name, value, schema_field, prefix='',
157                     mode='create'):
[8175]158        parts = value.split('-', 2)
159        country = ''
160        area = ''
161        ext = ''
162        if len(parts) == 3:
163            country = parts[0]
164            area = parts[1]
165            ext = parts[2]
166        elif len(parts) == 2:
167            country = parts[0]
168            ext = parts[1]
169        else:
170            ext = value
171        result = {
172            u'%s.country' % prefix: country,
173            u'%s.area' % prefix: area,
174            u'%s.ext' % prefix: ext}
175        return result
176
[7932]177class ResultEntryConverter(grok.Adapter):
178    grok.context(IResultEntryField)
[8214]179    grok.implements(IFieldConverter)
[7932]180
[8214]181    def request_data(self, name, value, schema_field, prefix='',
182                     mode='create'):
[7932]183        """Turn CSV values into ResultEntry-compatible form data.
184
185        Expects as `value` a _string_ like ``(u'mysubject',
186        u'mygrade')`` and turns it into some dict like::
187
188          {
189            'form.grade.subject': u'9234896395...',
190            'form.grade.grade': u'7e67e9e777..'
191            }
192
193        where the values are tokens from appropriate sources.
194
195        Such dicts can be transformed into real ResultEntry objects by
196        input widgets used in converters.
197        """
198        try:
199            entry = ResultEntry.from_string(value)
200            subj, grade = entry.subject, entry.grade
201        except:
202            return {prefix: value}
203        # web forms send tokens instead of real values
204        s_token = SubjectSource().factory.getToken(subj)
205        g_token = GradeSource().factory.getToken(grade)
206        result = {
207            "%ssubject" % (prefix): s_token,
208            "%sgrade" % (prefix): g_token,
209            }
210        return result
211
[6273]212class DefaultObjectConverter(grok.Adapter):
213    """Turn string values into real values.
[6260]214
[6273]215    A converter can convert string values for objects that implement a
216    certain interface into real values based on the given interface.
217    """
[6258]218
[6273]219    grok.context(Interface)
[8214]220    grok.implements(IObjectConverter)
[6263]221
[6273]222    def __init__(self, iface):
223        self.iface = iface
[7709]224        # Omit known dictionaries since there is no widget available
225        # for dictionary schema fields
226        self.default_form_fields = form.Fields(iface).omit('description_dict')
[6273]227        return
[6263]228
[8214]229    def fromStringDict(self, data_dict, context, form_fields=None,
230                       mode='create'):
[6273]231        """Convert values in `data_dict`.
[6263]232
[6273]233        Converts data in `data_dict` into real values based on
234        `context` and `form_fields`.
[6263]235
[6273]236        `data_dict` is a mapping (dict) from field names to values
237        represented as strings.
[6263]238
[6273]239        The fields (keys) to convert can be given in optional
240        `form_fields`. If given, form_fields should be an instance of
241        :class:`zope.formlib.form.Fields`. Suitable instances are for
242        example created by :class:`grok.AutoFields`.
[6263]243
[6273]244        If no `form_fields` are given, a default is computed from the
245        associated interface.
[6263]246
[6273]247        The `context` can be an existing object (implementing the
248        associated interface) or a factory name. If it is a string, we
249        try to create an object using
250        :func:`zope.component.createObject`.
[6263]251
[6273]252        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
253        <DATA_DICT>)`` where
[6263]254
[6273]255        ``<FIELD_ERRORS>``
256           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
257           error that happened when validating the input data in
258           `data_dict`
[6258]259
[6273]260        ``<INVARIANT_ERRORS>``
261           is a list of invariant errors concerning several fields
[6258]262
[6273]263        ``<DATA_DICT>``
264           is a dict with the values from input dict converted.
265
[8215]266        If mode is ``'create'`` or ``'update'`` then some additional
267        filtering applies:
268
269        - values set to DELETION_MARKER are set to missing_value (or
270          default value if field is required) and
271
272        - values set to IGNORE_MARKER are ignored and thus not part of
273          the returned ``<DATA_DICT>``.
274
[6273]275        If errors happen, i.e. the error lists are not empty, always
276        an empty ``<DATA_DICT>`` is returned.
277
[7597]278        If ``<DATA_DICT>`` is non-empty, there were no errors.
[6273]279        """
[6263]280        if form_fields is None:
[6273]281            form_fields = self.default_form_fields
[6263]282
[6273]283        request = TestRequest(form={})
[8214]284        new_data = dict()
[6273]285        for key, val in data_dict.items():
[7932]286            field = form_fields.get(key, None)
287            if field is not None:
288                # let adapters to the respective schema fields do the
289                # further fake-request processing
290                schema_field = field.interface[field.__name__]
291                field_converter = IFieldConverter(schema_field)
[8215]292                if mode in ('update', 'create'):
293                    if val == IGNORE_MARKER:
294                        continue
295                    elif val == DELETION_MARKER:
[8214]296                        val = schema_field.missing_value
297                        if schema_field.required:
298                            val = schema_field.default
299                        new_data[key] = val
300                        continue
[7932]301                request.form.update(
302                    field_converter.request_data(
303                        key, val, schema_field, 'form.%s' % key)
304                    )
305            else:
306                request.form['form.%s' % key] = val
[6273]307
[6263]308        obj = context
309        if isinstance(context, basestring):
[8335]310            # If we log initialization transitions in the __init__
311            # method of objects, a second (misleading) log entry
312            # will be created here.
[6263]313            obj = createObject(context)
[6273]314
315        widgets = form.setUpInputWidgets(
[6263]316            form_fields, 'form', obj, request)
[6273]317
318        errors = getWidgetsData(widgets, 'form', new_data)
319
320        invariant_errors = form.checkInvariants(form_fields, new_data)
[7932]321
[6273]322        if errors or invariant_errors:
323            err_messages = [(key, err.args[0]) for key, err in errors]
324            invariant_errors = [err.message for err in invariant_errors]
325            return err_messages, invariant_errors, {}
326
327        return errors, invariant_errors, new_data
Note: See TracBrowser for help on using the repository browser.