source: main/waeup.kofa/trunk/src/waeup/kofa/utils/converters.py @ 8175

Last change on this file since 8175 was 8175, checked in by uli, 13 years ago

Add schema field converter for phone numbers.

  • Property svn:keywords set to Id
File size: 10.3 KB
Line 
1## $Id: converters.py 8175 2012-04-16 09:16:13Z uli $
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
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)
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
103            try:
104                data[name] = widget.getInputValue()
105            except ValidationError, error:
106                # convert field ValidationError to WidgetInputError
107                error = WidgetInputError(widget.name, widget.label, error)
108                errors.append((name, error))
109            except InputErrors, error:
110                errors.append((name, error))
111
112    return errors
113
114class DefaultFieldConverter(grok.Adapter):
115    grok.context(Interface)
116    grok.provides(IFieldConverter)
117
118    def request_data(self, name, value, schema_field, prefix=''):
119        return {prefix: value}
120
121class ListFieldConverter(grok.Adapter):
122    grok.context(IList)
123    grok.provides(IFieldConverter)
124
125    def request_data(self, name, value, schema_field, prefix=''):
126        value_type = schema_field.value_type
127        try:
128            items = eval(value)
129        except:
130            return {prefix: value}
131        result = {'%s.count' % prefix: len(items)}
132        for num, item in enumerate(items):
133            sub_converter = IFieldConverter(value_type)
134            result.update(sub_converter.request_data(
135                unicode(num), unicode(item),
136                value_type, "%s.%s." % (prefix, num)))
137        return result
138
139class PhoneNumberFieldConverter(grok.Adapter):
140    """Convert strings into dict as expected from forms feeding PhoneWidget.
141
142    If you want strings without extra-checks imported, you can use
143    schema.TextLine in your interface instead of PhoneNumber.
144    """
145    grok.context(IPhoneNumber)
146    grok.provides(IFieldConverter)
147
148    def request_data(self, name, value, schema_field, prefix=''):
149        parts = value.split('-', 2)
150        country = ''
151        area = ''
152        ext = ''
153        if len(parts) == 3:
154            country = parts[0]
155            area = parts[1]
156            ext = parts[2]
157        elif len(parts) == 2:
158            country = parts[0]
159            ext = parts[1]
160        else:
161            ext = value
162        result = {
163            u'%s.country' % prefix: country,
164            u'%s.area' % prefix: area,
165            u'%s.ext' % prefix: ext}
166        return result
167
168class ResultEntryConverter(grok.Adapter):
169    grok.context(IResultEntryField)
170    grok.provides(IFieldConverter)
171
172    def request_data(self, name, value, schema_field, prefix=''):
173        """Turn CSV values into ResultEntry-compatible form data.
174
175        Expects as `value` a _string_ like ``(u'mysubject',
176        u'mygrade')`` and turns it into some dict like::
177
178          {
179            'form.grade.subject': u'9234896395...',
180            'form.grade.grade': u'7e67e9e777..'
181            }
182
183        where the values are tokens from appropriate sources.
184
185        Such dicts can be transformed into real ResultEntry objects by
186        input widgets used in converters.
187        """
188        try:
189            entry = ResultEntry.from_string(value)
190            subj, grade = entry.subject, entry.grade
191        except:
192            return {prefix: value}
193        # web forms send tokens instead of real values
194        s_token = SubjectSource().factory.getToken(subj)
195        g_token = GradeSource().factory.getToken(grade)
196        result = {
197            "%ssubject" % (prefix): s_token,
198            "%sgrade" % (prefix): g_token,
199            }
200        return result
201
202class DefaultObjectConverter(grok.Adapter):
203    """Turn string values into real values.
204
205    A converter can convert string values for objects that implement a
206    certain interface into real values based on the given interface.
207    """
208
209    grok.context(Interface)
210    grok.provides(IObjectConverter)
211
212    def __init__(self, iface):
213        self.iface = iface
214        # Omit known dictionaries since there is no widget available
215        # for dictionary schema fields
216        self.default_form_fields = form.Fields(iface).omit('description_dict')
217        return
218
219    def fromStringDict(self, data_dict, context, form_fields=None):
220        """Convert values in `data_dict`.
221
222        Converts data in `data_dict` into real values based on
223        `context` and `form_fields`.
224
225        `data_dict` is a mapping (dict) from field names to values
226        represented as strings.
227
228        The fields (keys) to convert can be given in optional
229        `form_fields`. If given, form_fields should be an instance of
230        :class:`zope.formlib.form.Fields`. Suitable instances are for
231        example created by :class:`grok.AutoFields`.
232
233        If no `form_fields` are given, a default is computed from the
234        associated interface.
235
236        The `context` can be an existing object (implementing the
237        associated interface) or a factory name. If it is a string, we
238        try to create an object using
239        :func:`zope.component.createObject`.
240
241        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
242        <DATA_DICT>)`` where
243
244        ``<FIELD_ERRORS>``
245           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
246           error that happened when validating the input data in
247           `data_dict`
248
249        ``<INVARIANT_ERRORS>``
250           is a list of invariant errors concerning several fields
251
252        ``<DATA_DICT>``
253           is a dict with the values from input dict converted.
254
255        If errors happen, i.e. the error lists are not empty, always
256        an empty ``<DATA_DICT>`` is returned.
257
258        If ``<DATA_DICT>`` is non-empty, there were no errors.
259        """
260        if form_fields is None:
261            form_fields = self.default_form_fields
262
263        request = TestRequest(form={})
264        for key, val in data_dict.items():
265            field = form_fields.get(key, None)
266            if field is not None:
267                # let adapters to the respective schema fields do the
268                # further fake-request processing
269                schema_field = field.interface[field.__name__]
270                field_converter = IFieldConverter(schema_field)
271                request.form.update(
272                    field_converter.request_data(
273                        key, val, schema_field, 'form.%s' % key)
274                    )
275            else:
276                request.form['form.%s' % key] = val
277
278        obj = context
279        if isinstance(context, basestring):
280            obj = createObject(context)
281
282        widgets = form.setUpInputWidgets(
283            form_fields, 'form', obj, request)
284
285        new_data = dict()
286        errors = getWidgetsData(widgets, 'form', new_data)
287
288        invariant_errors = form.checkInvariants(form_fields, new_data)
289
290        if errors or invariant_errors:
291            err_messages = [(key, err.args[0]) for key, err in errors]
292            invariant_errors = [err.message for err in invariant_errors]
293            return err_messages, invariant_errors, {}
294
295        return errors, invariant_errors, new_data
Note: See TracBrowser for help on using the repository browser.