source: main/waeup.kofa/branches/henrik-transcript-workflow/src/waeup/kofa/utils/converters.py @ 16250

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

Modify getWidgetsData so that we always get the same error format.

Fix tests.

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