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

Last change on this file since 12792 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
Line 
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##
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, IFieldConverter,
33    DELETION_MARKER, IGNORE_MARKER)
34from waeup.ikoba.schema.interfaces import IPhoneNumber
35from waeup.ikoba.products.productoptions import (
36    IProductOptionField, ProductOption)
37
38class ExtendedCheckBoxWidget(CheckBoxWidget):
39    """A checkbox widget that supports more input values as True/False
40    markers.
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
79def getWidgetsData(widgets, form_prefix, data):
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
91    :mod:`zope.formlib.form`. The first difference is that we also
92    store the fieldname for which a validation error happened in the
93    returned error list (what the original does not do).
94    The second difference is that we return only ConversionError objects.
95    """
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:
109                error = ConversionError(u'Validation failed')
110                errors.append((name, error))
111            except WidgetInputError, error:
112                error = ConversionError(u'Invalid input')
113                errors.append((name, error))
114            except ConversionError, error:
115                errors.append((name, error))
116
117    return errors
118
119class DefaultFieldConverter(grok.Adapter):
120    grok.context(Interface)
121    grok.implements(IFieldConverter)
122
123    def request_data(self, name, value, schema_field, prefix='',
124                     mode='create'):
125        if prefix == 'form.sex' and isinstance(value, basestring):
126            value = value.lower()
127        return {prefix: value}
128
129class ListFieldConverter(grok.Adapter):
130    grok.context(IList)
131    grok.implements(IFieldConverter)
132
133    def request_data(self, name, value, schema_field, prefix='',
134                     mode='create'):
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
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)
155    grok.implements(IFieldConverter)
156
157    def request_data(self, name, value, schema_field, prefix='',
158                     mode='create'):
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
178class ProductOptionConverter(grok.Adapter):
179    grok.context(IProductOptionField)
180    grok.implements(IFieldConverter)
181
182    def request_data(self, name, value, schema_field, prefix='',
183                     mode='create'):
184        """Turn CSV values into ProductOption-compatible form data.
185
186        Expects as `value` a _string_ like ``(u'mytitle',
187        u'myfee')`` and turns it into some dict like::
188
189          {
190            'form.option.title': u'9234896395...',
191            'form.option.fee': 7698769
192            'form.option.currency': u'7e67e9e777..'
193            }
194
195        where the values are tokens from appropriate sources.
196
197        Such dicts can be transformed into real ProductOption objects by
198        input widgets used in converters.
199        """
200        try:
201            entry = ProductOption.from_string(value)
202            title, fee, currency = entry.title, entry.fee, entry.currency
203        except:
204            return {prefix: value}
205        result = {
206            "%stitle" % (prefix): title,
207            "%sfee" % (prefix): fee,
208            "%scurrency" % (prefix): currency,
209            }
210        return result
211
212class DefaultObjectConverter(grok.Adapter):
213    """Turn string values into real values.
214
215    A converter can convert string values for objects that implement a
216    certain interface into real values based on the given interface.
217    """
218
219    grok.context(Interface)
220    grok.implements(IObjectConverter)
221
222    def __init__(self, iface):
223        self.iface = iface
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')
227        return
228
229    def fromStringDict(self, data_dict, context, form_fields=None,
230                       mode='create'):
231        """Convert values in `data_dict`.
232
233        Converts data in `data_dict` into real values based on
234        `context` and `form_fields`.
235
236        `data_dict` is a mapping (dict) from field names to values
237        represented as strings.
238
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`.
243
244        If no `form_fields` are given, a default is computed from the
245        associated interface.
246
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`.
251
252        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
253        <DATA_DICT>)`` where
254
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`
259
260        ``<INVARIANT_ERRORS>``
261           is a list of invariant errors concerning several fields
262
263        ``<DATA_DICT>``
264           is a dict with the values from input dict converted.
265
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
275        If errors happen, i.e. the error lists are not empty, always
276        an empty ``<DATA_DICT>`` is returned.
277
278        If ``<DATA_DICT>`` is non-empty, there were no errors.
279        """
280        if form_fields is None:
281            form_fields = self.default_form_fields
282
283        request = TestRequest(form={})
284        new_data = dict()
285        for key, val in data_dict.items():
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)
292                if mode in ('update', 'create'):
293                    if val == IGNORE_MARKER:
294                        continue
295                    elif val == DELETION_MARKER:
296                        val = schema_field.missing_value
297                        if schema_field.required:
298                            val = schema_field.default
299                        new_data[key] = val
300                        continue
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
307
308        obj = context
309        if isinstance(context, basestring):
310            # If we log initialization transitions in the __init__
311            # method of objects, a second (misleading) log entry
312            # will be created here.
313            obj = createObject(context)
314
315        widgets = form.setUpInputWidgets(
316            form_fields, 'form', obj, request)
317
318        errors = getWidgetsData(widgets, 'form', new_data)
319
320        invariant_errors = form.checkInvariants(form_fields, new_data)
321
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.