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

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

Adjust import paths.

  • Property svn:keywords set to Id
File size: 11.6 KB
RevLine 
[7196]1## $Id: converters.py 12309 2014-12-24 07:51:36Z 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 (
[12306]32    IObjectConverter, IProductOptionEntryField, IFieldConverter,
33    DELETION_MARKER, IGNORE_MARKER)
[11949]34from waeup.ikoba.schema.interfaces import IPhoneNumber
[12309]35from waeup.ikoba.products.productoptions import ProductOptionEntry
[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
[12306]177class ProductOptionEntryConverter(grok.Adapter):
178    grok.context(IProductOptionEntryField)
[8214]179    grok.implements(IFieldConverter)
[7932]180
[8214]181    def request_data(self, name, value, schema_field, prefix='',
182                     mode='create'):
[12306]183        """Turn CSV values into ProductOptionEntry-compatible form data.
[7932]184
[12306]185        Expects as `value` a _string_ like ``(u'mytitle',
186        u'myfee')`` and turns it into some dict like::
[7932]187
188          {
[12306]189            'form.option.title': u'9234896395...',
190            'form.option.fee': 7698769
191            'form.option.currency': u'7e67e9e777..'
[7932]192            }
193
194        where the values are tokens from appropriate sources.
195
[12306]196        Such dicts can be transformed into real ProductOptionEntry objects by
[7932]197        input widgets used in converters.
198        """
199        try:
[12306]200            entry = ProductOptionEntry.from_string(value)
201            title, fee, currency = entry.title, entry.fee, entry.currency
[7932]202        except:
203            return {prefix: value}
204        result = {
[12306]205            "%stitle" % (prefix): title,
206            "%sfee" % (prefix): fee,
207            "%scurrency" % (prefix): currency,
[7932]208            }
209        return result
210
[6273]211class DefaultObjectConverter(grok.Adapter):
212    """Turn string values into real values.
[6260]213
[6273]214    A converter can convert string values for objects that implement a
215    certain interface into real values based on the given interface.
216    """
[6258]217
[6273]218    grok.context(Interface)
[8214]219    grok.implements(IObjectConverter)
[6263]220
[6273]221    def __init__(self, iface):
222        self.iface = iface
[7709]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')
[6273]226        return
[6263]227
[8214]228    def fromStringDict(self, data_dict, context, form_fields=None,
229                       mode='create'):
[6273]230        """Convert values in `data_dict`.
[6263]231
[6273]232        Converts data in `data_dict` into real values based on
233        `context` and `form_fields`.
[6263]234
[6273]235        `data_dict` is a mapping (dict) from field names to values
236        represented as strings.
[6263]237
[6273]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`.
[6263]242
[6273]243        If no `form_fields` are given, a default is computed from the
244        associated interface.
[6263]245
[6273]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`.
[6263]250
[6273]251        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
252        <DATA_DICT>)`` where
[6263]253
[6273]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`
[6258]258
[6273]259        ``<INVARIANT_ERRORS>``
260           is a list of invariant errors concerning several fields
[6258]261
[6273]262        ``<DATA_DICT>``
263           is a dict with the values from input dict converted.
264
[8215]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
[6273]274        If errors happen, i.e. the error lists are not empty, always
275        an empty ``<DATA_DICT>`` is returned.
276
[7597]277        If ``<DATA_DICT>`` is non-empty, there were no errors.
[6273]278        """
[6263]279        if form_fields is None:
[6273]280            form_fields = self.default_form_fields
[6263]281
[6273]282        request = TestRequest(form={})
[8214]283        new_data = dict()
[6273]284        for key, val in data_dict.items():
[7932]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)
[8215]291                if mode in ('update', 'create'):
292                    if val == IGNORE_MARKER:
293                        continue
294                    elif val == DELETION_MARKER:
[8214]295                        val = schema_field.missing_value
296                        if schema_field.required:
297                            val = schema_field.default
298                        new_data[key] = val
299                        continue
[7932]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
[6273]306
[6263]307        obj = context
308        if isinstance(context, basestring):
[8335]309            # If we log initialization transitions in the __init__
310            # method of objects, a second (misleading) log entry
311            # will be created here.
[6263]312            obj = createObject(context)
[6273]313
314        widgets = form.setUpInputWidgets(
[6263]315            form_fields, 'form', obj, request)
[6273]316
317        errors = getWidgetsData(widgets, 'form', new_data)
318
319        invariant_errors = form.checkInvariants(form_fields, new_data)
[7932]320
[6273]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.