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

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

Turn ResultEntry? (school grades) components into ProductOptionEntry? components.

  • Property svn:keywords set to Id
File size: 11.6 KB
Line 
1## $Id: converters.py 12306 2014-12-24 07:38:53Z 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,
26    expandPrefix)
27from zope.formlib.interfaces import IInputWidget, ConversionError
28from zope.interface import Interface
29from zope.publisher.browser import TestRequest
30from zope.schema.interfaces import IList
31from waeup.ikoba.interfaces import (
32    IObjectConverter, IProductOptionEntryField, IFieldConverter,
33    DELETION_MARKER, IGNORE_MARKER)
34from waeup.ikoba.schema.interfaces import IPhoneNumber
35from waeup.ikoba.schoolgrades import ProductOptionEntry
36
37class ExtendedCheckBoxWidget(CheckBoxWidget):
38    """A checkbox widget that supports more input values as True/False
39    markers.
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
78def getWidgetsData(widgets, form_prefix, data):
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
90    :mod:`zope.formlib.form`. The first difference is that we also
91    store the fieldname for which a validation error happened in the
92    returned error list (what the original does not do).
93    The second difference is that we return only ConversionError objects.
94    """
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:
108                error = ConversionError(u'Validation failed')
109                errors.append((name, error))
110            except WidgetInputError, error:
111                error = ConversionError(u'Invalid input')
112                errors.append((name, error))
113            except ConversionError, error:
114                errors.append((name, error))
115
116    return errors
117
118class DefaultFieldConverter(grok.Adapter):
119    grok.context(Interface)
120    grok.implements(IFieldConverter)
121
122    def request_data(self, name, value, schema_field, prefix='',
123                     mode='create'):
124        if prefix == 'form.sex' and isinstance(value, basestring):
125            value = value.lower()
126        return {prefix: value}
127
128class ListFieldConverter(grok.Adapter):
129    grok.context(IList)
130    grok.implements(IFieldConverter)
131
132    def request_data(self, name, value, schema_field, prefix='',
133                     mode='create'):
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
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)
154    grok.implements(IFieldConverter)
155
156    def request_data(self, name, value, schema_field, prefix='',
157                     mode='create'):
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
177class ProductOptionEntryConverter(grok.Adapter):
178    grok.context(IProductOptionEntryField)
179    grok.implements(IFieldConverter)
180
181    def request_data(self, name, value, schema_field, prefix='',
182                     mode='create'):
183        """Turn CSV values into ProductOptionEntry-compatible form data.
184
185        Expects as `value` a _string_ like ``(u'mytitle',
186        u'myfee')`` and turns it into some dict like::
187
188          {
189            'form.option.title': u'9234896395...',
190            'form.option.fee': 7698769
191            'form.option.currency': u'7e67e9e777..'
192            }
193
194        where the values are tokens from appropriate sources.
195
196        Such dicts can be transformed into real ProductOptionEntry objects by
197        input widgets used in converters.
198        """
199        try:
200            entry = ProductOptionEntry.from_string(value)
201            title, fee, currency = entry.title, entry.fee, entry.currency
202        except:
203            return {prefix: value}
204        result = {
205            "%stitle" % (prefix): title,
206            "%sfee" % (prefix): fee,
207            "%scurrency" % (prefix): currency,
208            }
209        return result
210
211class DefaultObjectConverter(grok.Adapter):
212    """Turn string values into real values.
213
214    A converter can convert string values for objects that implement a
215    certain interface into real values based on the given interface.
216    """
217
218    grok.context(Interface)
219    grok.implements(IObjectConverter)
220
221    def __init__(self, iface):
222        self.iface = iface
223        # Omit known dictionaries since there is no widget available
224        # for dictionary schema fields
225        self.default_form_fields = form.Fields(iface).omit('description_dict')
226        return
227
228    def fromStringDict(self, data_dict, context, form_fields=None,
229                       mode='create'):
230        """Convert values in `data_dict`.
231
232        Converts data in `data_dict` into real values based on
233        `context` and `form_fields`.
234
235        `data_dict` is a mapping (dict) from field names to values
236        represented as strings.
237
238        The fields (keys) to convert can be given in optional
239        `form_fields`. If given, form_fields should be an instance of
240        :class:`zope.formlib.form.Fields`. Suitable instances are for
241        example created by :class:`grok.AutoFields`.
242
243        If no `form_fields` are given, a default is computed from the
244        associated interface.
245
246        The `context` can be an existing object (implementing the
247        associated interface) or a factory name. If it is a string, we
248        try to create an object using
249        :func:`zope.component.createObject`.
250
251        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
252        <DATA_DICT>)`` where
253
254        ``<FIELD_ERRORS>``
255           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
256           error that happened when validating the input data in
257           `data_dict`
258
259        ``<INVARIANT_ERRORS>``
260           is a list of invariant errors concerning several fields
261
262        ``<DATA_DICT>``
263           is a dict with the values from input dict converted.
264
265        If mode is ``'create'`` or ``'update'`` then some additional
266        filtering applies:
267
268        - values set to DELETION_MARKER are set to missing_value (or
269          default value if field is required) and
270
271        - values set to IGNORE_MARKER are ignored and thus not part of
272          the returned ``<DATA_DICT>``.
273
274        If errors happen, i.e. the error lists are not empty, always
275        an empty ``<DATA_DICT>`` is returned.
276
277        If ``<DATA_DICT>`` is non-empty, there were no errors.
278        """
279        if form_fields is None:
280            form_fields = self.default_form_fields
281
282        request = TestRequest(form={})
283        new_data = dict()
284        for key, val in data_dict.items():
285            field = form_fields.get(key, None)
286            if field is not None:
287                # let adapters to the respective schema fields do the
288                # further fake-request processing
289                schema_field = field.interface[field.__name__]
290                field_converter = IFieldConverter(schema_field)
291                if mode in ('update', 'create'):
292                    if val == IGNORE_MARKER:
293                        continue
294                    elif val == DELETION_MARKER:
295                        val = schema_field.missing_value
296                        if schema_field.required:
297                            val = schema_field.default
298                        new_data[key] = val
299                        continue
300                request.form.update(
301                    field_converter.request_data(
302                        key, val, schema_field, 'form.%s' % key)
303                    )
304            else:
305                request.form['form.%s' % key] = val
306
307        obj = context
308        if isinstance(context, basestring):
309            # If we log initialization transitions in the __init__
310            # method of objects, a second (misleading) log entry
311            # will be created here.
312            obj = createObject(context)
313
314        widgets = form.setUpInputWidgets(
315            form_fields, 'form', obj, request)
316
317        errors = getWidgetsData(widgets, 'form', new_data)
318
319        invariant_errors = form.checkInvariants(form_fields, new_data)
320
321        if errors or invariant_errors:
322            err_messages = [(key, err.args[0]) for key, err in errors]
323            invariant_errors = [err.message for err in invariant_errors]
324            return err_messages, invariant_errors, {}
325
326        return errors, invariant_errors, new_data
Note: See TracBrowser for help on using the repository browser.