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

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

Move ProductOption? interfaces to productoptions to avoid nasty circular imports.

Use ISO_4217_CURRENCIES.

  • Property svn:keywords set to Id
File size: 11.6 KB
RevLine 
[7196]1## $Id: converters.py 12343 2014-12-30 12:52:40Z 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 (
[12343]32    IObjectConverter, IFieldConverter,
[12306]33    DELETION_MARKER, IGNORE_MARKER)
[11949]34from waeup.ikoba.schema.interfaces import IPhoneNumber
[12343]35from waeup.ikoba.products.productoptions import (
36    IProductOptionField, ProductOption)
[6258]37
[6278]38class ExtendedCheckBoxWidget(CheckBoxWidget):
39    """A checkbox widget that supports more input values as True/False
[7597]40    markers.
[6278]41
42    The default bool widget expects the string 'on' as only valid
43    ``True`` value in HTML forms for bool fields.
44
45    This widget also accepts '1', 'true' and 'yes' for that. Also all
46    uppercase/lowecase combinations of these strings are accepted.
47
48    The widget still renders ``True`` to ``'on'`` when a form is
49    generated.
50    """
51    true_markers = ['1', 'true', 'on', 'yes']
52
53    def _toFieldValue(self, input):
54        """Convert from HTML presentation to Python bool."""
55        if not isinstance(input, basestring):
56            return False
57        return input.lower() in self.true_markers
58
59    def _getFormInput(self):
60        """Returns the form input used by `_toFieldValue`.
61
62        Return values:
63
64          ``'on'``  checkbox is checked
65          ``''``    checkbox is not checked
66          ``None``  form input was not provided
67
68        """
69        value = self.request.get(self.name)
70        if isinstance(value, basestring):
71            value = value.lower()
72        if value in self.true_markers:
73            return 'on'
74        elif self.name + '.used' in self.request:
75            return ''
76        else:
77            return None
78
[6260]79def getWidgetsData(widgets, form_prefix, data):
[6273]80    """Get data and validation errors from `widgets` for `data`.
81
82    Updates the dict in `data` with values from the widgets in
83    `widgets`.
84
85    Returns a list of tuples ``(<WIDGET_NAME>, <ERROR>)`` where
86    ``<WIDGET_NAME>`` is a widget name (normally the same as the
87    associated field name) and ``<ERROR>`` is the exception that
88    happened for that widget/field.
89
90    This is merely a copy from the same-named function in
[12261]91    :mod:`zope.formlib.form`. The first difference is that we also
[6273]92    store the fieldname for which a validation error happened in the
93    returned error list (what the original does not do).
[12261]94    The second difference is that we return only ConversionError objects.
[6273]95    """
[6260]96    errors = []
97    form_prefix = expandPrefix(form_prefix)
98
99    for input, widget in widgets.__iter_input_and_widget__():
100        if input and IInputWidget.providedBy(widget):
101            name = _widgetKey(widget, form_prefix)
102
103            if not widget.hasInput():
104                continue
105
106            try:
107                data[name] = widget.getInputValue()
108            except ValidationError, error:
[12261]109                error = ConversionError(u'Validation failed')
[6260]110                errors.append((name, error))
[12261]111            except WidgetInputError, error:
112                error = ConversionError(u'Invalid input')
[6260]113                errors.append((name, error))
[12261]114            except ConversionError, error:
115                errors.append((name, error))
[6260]116
117    return errors
118
[7932]119class DefaultFieldConverter(grok.Adapter):
120    grok.context(Interface)
[8214]121    grok.implements(IFieldConverter)
[6278]122
[8214]123    def request_data(self, name, value, schema_field, prefix='',
124                     mode='create'):
[9932]125        if prefix == 'form.sex' and isinstance(value, basestring):
126            value = value.lower()
[7932]127        return {prefix: value}
128
129class ListFieldConverter(grok.Adapter):
130    grok.context(IList)
[8214]131    grok.implements(IFieldConverter)
[7932]132
[8214]133    def request_data(self, name, value, schema_field, prefix='',
134                     mode='create'):
[7932]135        value_type = schema_field.value_type
136        try:
137            items = eval(value)
138        except:
139            return {prefix: value}
140        result = {'%s.count' % prefix: len(items)}
141        for num, item in enumerate(items):
142            sub_converter = IFieldConverter(value_type)
143            result.update(sub_converter.request_data(
144                unicode(num), unicode(item),
145                value_type, "%s.%s." % (prefix, num)))
146        return result
147
[8175]148class PhoneNumberFieldConverter(grok.Adapter):
149    """Convert strings into dict as expected from forms feeding PhoneWidget.
150
151    If you want strings without extra-checks imported, you can use
152    schema.TextLine in your interface instead of PhoneNumber.
153    """
154    grok.context(IPhoneNumber)
[8214]155    grok.implements(IFieldConverter)
[8175]156
[8214]157    def request_data(self, name, value, schema_field, prefix='',
158                     mode='create'):
[8175]159        parts = value.split('-', 2)
160        country = ''
161        area = ''
162        ext = ''
163        if len(parts) == 3:
164            country = parts[0]
165            area = parts[1]
166            ext = parts[2]
167        elif len(parts) == 2:
168            country = parts[0]
169            ext = parts[1]
170        else:
171            ext = value
172        result = {
173            u'%s.country' % prefix: country,
174            u'%s.area' % prefix: area,
175            u'%s.ext' % prefix: ext}
176        return result
177
[12327]178class ProductOptionConverter(grok.Adapter):
179    grok.context(IProductOptionField)
[8214]180    grok.implements(IFieldConverter)
[7932]181
[8214]182    def request_data(self, name, value, schema_field, prefix='',
183                     mode='create'):
[12327]184        """Turn CSV values into ProductOption-compatible form data.
[7932]185
[12306]186        Expects as `value` a _string_ like ``(u'mytitle',
187        u'myfee')`` and turns it into some dict like::
[7932]188
189          {
[12306]190            'form.option.title': u'9234896395...',
191            'form.option.fee': 7698769
192            'form.option.currency': u'7e67e9e777..'
[7932]193            }
194
195        where the values are tokens from appropriate sources.
196
[12327]197        Such dicts can be transformed into real ProductOption objects by
[7932]198        input widgets used in converters.
199        """
200        try:
[12327]201            entry = ProductOption.from_string(value)
[12306]202            title, fee, currency = entry.title, entry.fee, entry.currency
[7932]203        except:
204            return {prefix: value}
205        result = {
[12306]206            "%stitle" % (prefix): title,
207            "%sfee" % (prefix): fee,
208            "%scurrency" % (prefix): currency,
[7932]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.