source: main/waeup.kofa/trunk/src/waeup/kofa/utils/converters.py @ 8517

Last change on this file since 8517 was 8335, checked in by Henrik Bettermann, 13 years ago

Move initial workflow transition to handle_applicant_added. This way we can avoid misleading duplicate log entries.

Roll back changes in workflow.py.

  • Property svn:keywords set to Id
File size: 11.4 KB
Line 
1## $Id: converters.py 8335 2012-05-03 20:53:09Z 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, expandPrefix)
26from zope.formlib.interfaces import IInputWidget
27from zope.interface import Interface
28from zope.publisher.browser import TestRequest
29from zope.schema.interfaces import IList
30from waeup.kofa.interfaces import (
31    IObjectConverter, IResultEntryField, IFieldConverter, SubjectSource,
32    GradeSource, DELETION_MARKER, IGNORE_MARKER)
33from waeup.kofa.schema.interfaces import IPhoneNumber
34from waeup.kofa.schoolgrades import ResultEntry
35
36class ExtendedCheckBoxWidget(CheckBoxWidget):
37    """A checkbox widget that supports more input values as True/False
38    markers.
39
40    The default bool widget expects the string 'on' as only valid
41    ``True`` value in HTML forms for bool fields.
42
43    This widget also accepts '1', 'true' and 'yes' for that. Also all
44    uppercase/lowecase combinations of these strings are accepted.
45
46    The widget still renders ``True`` to ``'on'`` when a form is
47    generated.
48    """
49    true_markers = ['1', 'true', 'on', 'yes']
50
51    def _toFieldValue(self, input):
52        """Convert from HTML presentation to Python bool."""
53        if not isinstance(input, basestring):
54            return False
55        return input.lower() in self.true_markers
56
57    def _getFormInput(self):
58        """Returns the form input used by `_toFieldValue`.
59
60        Return values:
61
62          ``'on'``  checkbox is checked
63          ``''``    checkbox is not checked
64          ``None``  form input was not provided
65
66        """
67        value = self.request.get(self.name)
68        if isinstance(value, basestring):
69            value = value.lower()
70        if value in self.true_markers:
71            return 'on'
72        elif self.name + '.used' in self.request:
73            return ''
74        else:
75            return None
76
77def getWidgetsData(widgets, form_prefix, data):
78    """Get data and validation errors from `widgets` for `data`.
79
80    Updates the dict in `data` with values from the widgets in
81    `widgets`.
82
83    Returns a list of tuples ``(<WIDGET_NAME>, <ERROR>)`` where
84    ``<WIDGET_NAME>`` is a widget name (normally the same as the
85    associated field name) and ``<ERROR>`` is the exception that
86    happened for that widget/field.
87
88    This is merely a copy from the same-named function in
89    :mod:`zope.formlib.form`. The only difference is that we also
90    store the fieldname for which a validation error happened in the
91    returned error list (what the original does not do).
92    """
93    errors = []
94    form_prefix = expandPrefix(form_prefix)
95
96    for input, widget in widgets.__iter_input_and_widget__():
97        if input and IInputWidget.providedBy(widget):
98            name = _widgetKey(widget, form_prefix)
99
100            if not widget.hasInput():
101                continue
102
103            try:
104                data[name] = widget.getInputValue()
105            except ValidationError, error:
106                # convert field ValidationError to WidgetInputError
107                error = WidgetInputError(widget.name, widget.label, error)
108                errors.append((name, error))
109            except InputErrors, error:
110                errors.append((name, error))
111
112    return errors
113
114class DefaultFieldConverter(grok.Adapter):
115    grok.context(Interface)
116    grok.implements(IFieldConverter)
117
118    def request_data(self, name, value, schema_field, prefix='',
119                     mode='create'):
120        return {prefix: value}
121
122class ListFieldConverter(grok.Adapter):
123    grok.context(IList)
124    grok.implements(IFieldConverter)
125
126    def request_data(self, name, value, schema_field, prefix='',
127                     mode='create'):
128        value_type = schema_field.value_type
129        try:
130            items = eval(value)
131        except:
132            return {prefix: value}
133        result = {'%s.count' % prefix: len(items)}
134        for num, item in enumerate(items):
135            sub_converter = IFieldConverter(value_type)
136            result.update(sub_converter.request_data(
137                unicode(num), unicode(item),
138                value_type, "%s.%s." % (prefix, num)))
139        return result
140
141class PhoneNumberFieldConverter(grok.Adapter):
142    """Convert strings into dict as expected from forms feeding PhoneWidget.
143
144    If you want strings without extra-checks imported, you can use
145    schema.TextLine in your interface instead of PhoneNumber.
146    """
147    grok.context(IPhoneNumber)
148    grok.implements(IFieldConverter)
149
150    def request_data(self, name, value, schema_field, prefix='',
151                     mode='create'):
152        parts = value.split('-', 2)
153        country = ''
154        area = ''
155        ext = ''
156        if len(parts) == 3:
157            country = parts[0]
158            area = parts[1]
159            ext = parts[2]
160        elif len(parts) == 2:
161            country = parts[0]
162            ext = parts[1]
163        else:
164            ext = value
165        result = {
166            u'%s.country' % prefix: country,
167            u'%s.area' % prefix: area,
168            u'%s.ext' % prefix: ext}
169        return result
170
171class ResultEntryConverter(grok.Adapter):
172    grok.context(IResultEntryField)
173    grok.implements(IFieldConverter)
174
175    def request_data(self, name, value, schema_field, prefix='',
176                     mode='create'):
177        """Turn CSV values into ResultEntry-compatible form data.
178
179        Expects as `value` a _string_ like ``(u'mysubject',
180        u'mygrade')`` and turns it into some dict like::
181
182          {
183            'form.grade.subject': u'9234896395...',
184            'form.grade.grade': u'7e67e9e777..'
185            }
186
187        where the values are tokens from appropriate sources.
188
189        Such dicts can be transformed into real ResultEntry objects by
190        input widgets used in converters.
191        """
192        try:
193            entry = ResultEntry.from_string(value)
194            subj, grade = entry.subject, entry.grade
195        except:
196            return {prefix: value}
197        # web forms send tokens instead of real values
198        s_token = SubjectSource().factory.getToken(subj)
199        g_token = GradeSource().factory.getToken(grade)
200        result = {
201            "%ssubject" % (prefix): s_token,
202            "%sgrade" % (prefix): g_token,
203            }
204        return result
205
206class DefaultObjectConverter(grok.Adapter):
207    """Turn string values into real values.
208
209    A converter can convert string values for objects that implement a
210    certain interface into real values based on the given interface.
211    """
212
213    grok.context(Interface)
214    grok.implements(IObjectConverter)
215
216    def __init__(self, iface):
217        self.iface = iface
218        # Omit known dictionaries since there is no widget available
219        # for dictionary schema fields
220        self.default_form_fields = form.Fields(iface).omit('description_dict')
221        return
222
223    def fromStringDict(self, data_dict, context, form_fields=None,
224                       mode='create'):
225        """Convert values in `data_dict`.
226
227        Converts data in `data_dict` into real values based on
228        `context` and `form_fields`.
229
230        `data_dict` is a mapping (dict) from field names to values
231        represented as strings.
232
233        The fields (keys) to convert can be given in optional
234        `form_fields`. If given, form_fields should be an instance of
235        :class:`zope.formlib.form.Fields`. Suitable instances are for
236        example created by :class:`grok.AutoFields`.
237
238        If no `form_fields` are given, a default is computed from the
239        associated interface.
240
241        The `context` can be an existing object (implementing the
242        associated interface) or a factory name. If it is a string, we
243        try to create an object using
244        :func:`zope.component.createObject`.
245
246        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
247        <DATA_DICT>)`` where
248
249        ``<FIELD_ERRORS>``
250           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
251           error that happened when validating the input data in
252           `data_dict`
253
254        ``<INVARIANT_ERRORS>``
255           is a list of invariant errors concerning several fields
256
257        ``<DATA_DICT>``
258           is a dict with the values from input dict converted.
259
260        If mode is ``'create'`` or ``'update'`` then some additional
261        filtering applies:
262
263        - values set to DELETION_MARKER are set to missing_value (or
264          default value if field is required) and
265
266        - values set to IGNORE_MARKER are ignored and thus not part of
267          the returned ``<DATA_DICT>``.
268
269        If errors happen, i.e. the error lists are not empty, always
270        an empty ``<DATA_DICT>`` is returned.
271
272        If ``<DATA_DICT>`` is non-empty, there were no errors.
273        """
274        if form_fields is None:
275            form_fields = self.default_form_fields
276
277        request = TestRequest(form={})
278        new_data = dict()
279        for key, val in data_dict.items():
280            field = form_fields.get(key, None)
281            if field is not None:
282                # let adapters to the respective schema fields do the
283                # further fake-request processing
284                schema_field = field.interface[field.__name__]
285                field_converter = IFieldConverter(schema_field)
286                if mode in ('update', 'create'):
287                    if val == IGNORE_MARKER:
288                        continue
289                    elif val == DELETION_MARKER:
290                        val = schema_field.missing_value
291                        if schema_field.required:
292                            val = schema_field.default
293                        new_data[key] = val
294                        continue
295                request.form.update(
296                    field_converter.request_data(
297                        key, val, schema_field, 'form.%s' % key)
298                    )
299            else:
300                request.form['form.%s' % key] = val
301
302        obj = context
303        if isinstance(context, basestring):
304            # If we log initialization transitions in the __init__
305            # method of objects, a second (misleading) log entry
306            # will be created here.
307            obj = createObject(context)
308
309        widgets = form.setUpInputWidgets(
310            form_fields, 'form', obj, request)
311
312        errors = getWidgetsData(widgets, 'form', new_data)
313
314        invariant_errors = form.checkInvariants(form_fields, new_data)
315
316        if errors or invariant_errors:
317            err_messages = [(key, err.args[0]) for key, err in errors]
318            invariant_errors = [err.message for err in invariant_errors]
319            return err_messages, invariant_errors, {}
320
321        return errors, invariant_errors, new_data
Note: See TracBrowser for help on using the repository browser.