source: main/waeup.sirp/trunk/src/waeup/sirp/utils/converters.py @ 6273

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

Finally make the new converter work. API-wise it is as good as the old one (can import everyting, the old one could),
but design-wise it might be much more powerfull. Basically it can handle/convert all content-types for which one can
create an Add- or EditForm? successfully. In other words: if you manage to write an edit form for some content type,
then you can also create an importer for that content-type. Still finetuning needed (for dates, bool data, etc.) but
the main things work.

File size: 12.1 KB
Line 
1"""Converters for zope.schema-based datatypes.
2"""
3import datetime
4import grok
5from zope.component import getMultiAdapter
6from zope.publisher.browser import TestRequest
7try:
8    from zope.app.form.browser.interfaces import ITerms
9except ImportError:
10    from zope.browser.interfaces import ITerms
11from zope.schema.interfaces import IBool, IText, IInt, IChoice, IDate
12from waeup.sirp.interfaces import ISchemaTypeConverter
13
14# If a string has this value, it is considered as 'missing_value' or None.
15NONE_STRING_VALUE = ''
16
17class Converter(grok.Adapter):
18    """Base for string-or-none to zope.schema field converters.
19    """
20    grok.baseclass()
21    grok.provides(ISchemaTypeConverter)
22
23    def __init__(self, context):
24        """Create a converter with context, a zope.schema.Field, as context.
25        """
26        self.context = context
27
28    def _convertValueFromString(self, string):
29        """You must at least override this method to build a working
30           converter.
31        """
32        raise NotImplementedError('method not implemented')
33
34    def fromString(self, string=None, strict=True):
35        """Convert ``string`` to value according to assigned field type.
36        """
37        result = None
38        if string is NONE_STRING_VALUE:
39            string = None
40        if string is None:
41            if self.context.required is True:
42                result = self.context.default
43            else:
44                result = self.context.missing_value
45        else:
46            result = self._convertValueFromString(string)
47        if strict:
48            self.context.validate(result)
49        return result
50
51    def _convertValueToString(self, value):
52        return str(value)
53
54    def toString(self, value, strict=True):
55        """Convert given `value` to string according to assigned field type.
56        """
57        if strict:
58            self.context.validate(value)
59        if value == self.context.missing_value:
60            return None
61        return self._convertValueToString(value)
62
63class BoolConverter(Converter):
64    """A converter for zope.schema.Bool fields.
65    """
66    grok.context(IBool)
67    grok.provides(ISchemaTypeConverter)
68
69    def _convertValueFromString(self, string):
70        if string is NONE_STRING_VALUE:
71            string = None
72        if string is None:
73            return None
74        if string.lower() in ['1', 'true', 'yes']:
75            return True
76        return False
77
78    def _convertValueToString(self, value):
79        if value is None:
80            return None
81        if value:
82            return '1'
83        return '0'
84
85class TextConverter(Converter):
86    """A converter for zope.schema.interfaces.IText fields.
87    """
88    grok.context(IText)
89    grok.provides(ISchemaTypeConverter)
90
91    def _convertValueFromString(self, string):
92        return unicode(string)
93
94class IntConverter(Converter):
95    """A converter for zope.schema.Int fields.
96    """
97    grok.context(IInt)
98    grok.provides(ISchemaTypeConverter)
99
100    def _convertValueFromString(self, string):
101        return int(string)
102
103class ChoiceConverter(Converter):
104    """A converter for zope.schema.Choice fields.
105    """
106    grok.context(IChoice)
107    grok.provides(ISchemaTypeConverter)
108
109    tokens = None
110    values = None
111
112    def __init__(self, context):
113        self.context = context
114        if not hasattr(self.context.source, 'factory'):
115            try:
116                self.terms = getMultiAdapter(
117                    (self.context.source, TestRequest()), ITerms)
118            except:
119                self.terms = None
120
121            return
122        if not hasattr(self.context.source.factory, 'getToken'):
123            return
124        # For expensive token/key lookups we create a 'cache'
125        # here. This speeds up mass operations with many conversions
126        # by factor 10 or more.
127
128        # Mapping token -> value
129        self.tokens = dict([(self.context.source.factory.getToken(x), x)
130                            for x in self.context.source.factory.getValues()])
131        # Mapping value -> token
132        self.values = dict([(y,x) for x,y in self.tokens.items()])
133
134    def _convertValueFromString(self, string):
135        if self.tokens is not None:
136            result = None
137            try:
138                result = self.tokens[string]
139            except KeyError:
140                # Be gentle...
141                try:
142                    result = self.tokens[string.lower()]
143                    return result
144                except KeyError:
145                    tokenlist = (','.join(self.tokens[:2]))
146                    raise ValueError(
147                        'The token %s is not valid. Use one of %s, ...' % (
148                            string, tokenlist))
149            return result
150        if self.terms is not None:
151            return self.terms.getValue(string)
152        result = self.context.source.getTermByToken(string).value
153        return result
154
155    def _convertValueToString(self, value):
156        if self.values is not None:
157            return self.values[value]
158        if self.terms is not None:
159            return self.terms.getTerm(value).token
160        return str(value)
161
162    def fromString(self, string=None, strict=False):
163        """Convert ``string`` to value according to assigned field type.
164
165        We change the default for ``strict``: this disables extra
166        validation checks for Choice fields and saves lots of time. If
167        a string/value is out of allowed range we get a value or key
168        error anyway.
169        """
170        return super(ChoiceConverter, self).fromString(string=string,
171                                                       strict=strict)
172
173    def toString(self, value, strict=False):
174        """Convert ``value`` to string according to assigned field type.
175
176        We change the default for ``strict``: this disables extra
177        validation checks for Choice fields and saves lots of time. If
178        a string/value is out of allowed range we get a value or key
179        error anyway.
180        """
181        return super(ChoiceConverter, self).toString(value=value,
182                                                     strict=strict)
183
184class DateConverter(Converter):
185    """A converter for zope.schema.IDate fields.
186
187    Converts date to string and vice versa. Stringified dates are
188    expected in format ``YYYY-MM-DD``.
189
190    To support at least some other formats, we accept for conversion
191    from string also the following formats:
192
193    * ``YYYY/MM/DD``
194
195    * ``DD/MM/YYYY``
196
197    * ``D/M/YYYY``
198
199    * ``DD.MM.YYYY``
200
201    * ``D.M.YYYY``
202
203    When converting to strings, always 'YYYY-MM-DD' is returned.
204
205    For convenience, when converting from strings also string data
206    separated by space is stripped before processing. Therefore
207    strings like '1990-04-01 12:12:01 GMT +1' will also work.
208    """
209    grok.context(IDate)
210    grok.provides(ISchemaTypeConverter)
211
212    #: List of supported date formats in `strftime()` notation.
213    formats = ['%Y-%m-%d', '%Y/%m/%d' , '%d/%m/%Y', '%D/%M/%Y',
214               '%d.%m.%Y', '%D.%M.%Y']
215
216    def _convertValueFromString(self, string):
217        orig_string = string
218        if string is NONE_STRING_VALUE:
219            string = None
220        if string is None:
221            return None
222        value = None
223        if ' ' in string:
224            string = string.split(' ')[0]
225        for format in self.formats:
226            try:
227                value = datetime.datetime.strptime(string, format)
228                break
229            except ValueError:
230                pass
231        if value is None:
232            raise ValueError(
233                'Cannot convert to date: %s. Use YYYY-MM-DD.' %
234                orig_string)
235        value = value.date()
236        return value
237
238    def _convertValueToString(self, value):
239        return datetime.date.strftime(value, '%Y-%m-%d')
240
241
242from zope import schema
243from zope.component import createObject
244from zope.interface import Interface
245from zope.formlib import form
246from zope.formlib.form import (
247    _widgetKey, WidgetInputError, ValidationError, InputErrors, expandPrefix,
248    setUpInputWidgets)
249from zope.formlib.interfaces import IInputWidget
250from zope.publisher.browser import TestRequest
251from waeup.sirp.interfaces import IObjectConverter
252
253def getWidgetsData(widgets, form_prefix, data):
254    """Get data and validation errors from `widgets` for `data`.
255
256    Updates the dict in `data` with values from the widgets in
257    `widgets`.
258
259    Returns a list of tuples ``(<WIDGET_NAME>, <ERROR>)`` where
260    ``<WIDGET_NAME>`` is a widget name (normally the same as the
261    associated field name) and ``<ERROR>`` is the exception that
262    happened for that widget/field.
263
264    This is merely a copy from the same-named function in
265    :mod:`zope.formlib.form`. The only difference is that we also
266    store the fieldname for which a validation error happened in the
267    returned error list (what the original does not do).
268
269    """
270    errors = []
271    form_prefix = expandPrefix(form_prefix)
272
273    for input, widget in widgets.__iter_input_and_widget__():
274        if input and IInputWidget.providedBy(widget):
275            name = _widgetKey(widget, form_prefix)
276
277            if not widget.hasInput():
278                continue
279
280            try:
281                data[name] = widget.getInputValue()
282            except ValidationError, error:
283                # convert field ValidationError to WidgetInputError
284                error = WidgetInputError(widget.name, widget.label, error)
285                errors.append((name, error))
286            except InputErrors, error:
287                errors.append((name, error))
288
289    return errors
290
291class DefaultObjectConverter(grok.Adapter):
292    """Turn string values into real values.
293
294    A converter can convert string values for objects that implement a
295    certain interface into real values based on the given interface.
296    """
297
298    grok.context(Interface)
299    grok.provides(IObjectConverter)
300
301    def __init__(self, iface):
302        self.iface = iface
303        self.default_form_fields = form.Fields(iface)
304        return
305
306    def fromStringDict(self, data_dict, context, form_fields=None):
307        """Convert values in `data_dict`.
308
309        Converts data in `data_dict` into real values based on
310        `context` and `form_fields`.
311
312        `data_dict` is a mapping (dict) from field names to values
313        represented as strings.
314
315        The fields (keys) to convert can be given in optional
316        `form_fields`. If given, form_fields should be an instance of
317        :class:`zope.formlib.form.Fields`. Suitable instances are for
318        example created by :class:`grok.AutoFields`.
319
320        If no `form_fields` are given, a default is computed from the
321        associated interface.
322
323        The `context` can be an existing object (implementing the
324        associated interface) or a factory name. If it is a string, we
325        try to create an object using
326        :func:`zope.component.createObject`.
327
328        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
329        <DATA_DICT>)`` where
330
331        ``<FIELD_ERRORS>``
332           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
333           error that happened when validating the input data in
334           `data_dict`
335
336        ``<INVARIANT_ERRORS>``
337           is a list of invariant errors concerning several fields
338
339        ``<DATA_DICT>``
340           is a dict with the values from input dict converted.
341
342        If errors happen, i.e. the error lists are not empty, always
343        an empty ``<DATA_DICT>`` is returned.
344
345        If ``<DATA_DICT>` is non-empty, there were no errors.
346        """
347        if form_fields is None:
348            form_fields = self.default_form_fields
349
350        request = TestRequest(form={})
351        for key, val in data_dict.items():
352            request.form['form.%s' % key] = val
353
354        obj = context
355        if isinstance(context, basestring):
356            obj = createObject(context)
357
358        widgets = form.setUpInputWidgets(
359            form_fields, 'form', obj, request)
360
361        new_data = dict()
362        errors = getWidgetsData(widgets, 'form', new_data)
363
364        invariant_errors = form.checkInvariants(form_fields, new_data)
365        if errors or invariant_errors:
366            err_messages = [(key, err.args[0]) for key, err in errors]
367            invariant_errors = [err.message for err in invariant_errors]
368            return err_messages, invariant_errors, {}
369
370        return errors, invariant_errors, new_data
Note: See TracBrowser for help on using the repository browser.