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

Last change on this file since 17766 was 17764, checked in by Henrik Bettermann, 6 months ago

Add RefereeEntryConverter?.

  • Property svn:keywords set to Id
File size: 12.3 KB
Line 
1## $Id: converters.py 17764 2024-05-11 19:56:39Z 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, ConversionError
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, IRefereeEntryField,
32    IFieldConverter, SubjectSource,
33    GradeSource, DELETION_MARKER, IGNORE_MARKER)
34from waeup.kofa.schema.interfaces import IPhoneNumber
35from waeup.kofa.schoolgrades import ResultEntry
36from waeup.kofa.refereeentries import RefereeEntry
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 only 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    """
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            try:
105                data[name] = widget.getInputValue()
106            except ValidationError, error:
107                error = ConversionError(u'Validation failed')
108                errors.append((name, error))
109            except WidgetInputError, error:
110                error = ConversionError(u'Invalid input')
111                errors.append((name, error))
112            except ConversionError, error:
113                errors.append((name, error))
114    return errors
115
116class DefaultFieldConverter(grok.Adapter):
117    grok.context(Interface)
118    grok.implements(IFieldConverter)
119
120    def request_data(self, name, value, schema_field, prefix='',
121                     mode='create'):
122        if prefix == 'form.sex' and isinstance(value, basestring):
123            value = value.lower()
124        return {prefix: value}
125
126class ListFieldConverter(grok.Adapter):
127    grok.context(IList)
128    grok.implements(IFieldConverter)
129
130    def request_data(self, name, value, schema_field, prefix='',
131                     mode='create'):
132        value_type = schema_field.value_type
133        try:
134            items = eval(value)
135        except:
136            return {prefix: value}
137        result = {'%s.count' % prefix: len(items)}
138        for num, item in enumerate(items):
139            sub_converter = IFieldConverter(value_type)
140            result.update(sub_converter.request_data(
141                unicode(num), unicode(item),
142                value_type, "%s.%s." % (prefix, num)))
143        return result
144
145class PhoneNumberFieldConverter(grok.Adapter):
146    """Convert strings into dict as expected from forms feeding PhoneWidget.
147
148    If you want strings without extra-checks imported, you can use
149    schema.TextLine in your interface instead of PhoneNumber.
150    """
151    grok.context(IPhoneNumber)
152    grok.implements(IFieldConverter)
153
154    def request_data(self, name, value, schema_field, prefix='',
155                     mode='create'):
156        parts = value.split('-', 2)
157        country = ''
158        area = ''
159        ext = ''
160        if len(parts) == 3:
161            country = parts[0]
162            area = parts[1]
163            ext = parts[2]
164        elif len(parts) == 2:
165            country = parts[0]
166            ext = parts[1]
167        else:
168            ext = value
169        result = {
170            u'%s.country' % prefix: country,
171            u'%s.area' % prefix: area,
172            u'%s.ext' % prefix: ext}
173        return result
174
175class ResultEntryConverter(grok.Adapter):
176    grok.context(IResultEntryField)
177    grok.implements(IFieldConverter)
178
179    def request_data(self, name, value, schema_field, prefix='',
180                     mode='create'):
181        """Turn CSV values into ResultEntry-compatible form data.
182
183        Expects as `value` a _string_ like ``(u'mysubject',
184        u'mygrade')`` and turns it into some dict like::
185
186          {
187            'form.grade.subject': u'9234896395...',
188            'form.grade.grade': u'7e67e9e777..'
189            }
190
191        where the values are tokens from appropriate sources.
192
193        Such dicts can be transformed into real ResultEntry objects by
194        input widgets used in converters.
195        """
196        try:
197            entry = ResultEntry.from_string(value)
198            subj, grade = entry.subject, entry.grade
199        except:
200            return {prefix: value}
201        # web forms send tokens instead of real values
202        s_token = SubjectSource().factory.getToken(subj)
203        g_token = GradeSource().factory.getToken(grade)
204        result = {
205            "%ssubject" % (prefix): s_token,
206            "%sgrade" % (prefix): g_token,
207            }
208        return result
209
210class RefereeEntryConverter(grok.Adapter):
211    grok.context(IRefereeEntryField)
212    grok.implements(IFieldConverter)
213
214    def request_data(self, name, value, schema_field, prefix='',
215                     mode='create'):
216        """Turn CSV values into RefereeEntry-compatible form data.
217        See ResultEntryConverter.
218        """
219        try:
220            entry = RefereeEntry.from_string(value)
221            name, email, email_sent = entry.name, entry.email, entry.email_sent
222        except:
223            return {prefix: value}
224        result = {
225            "%sname" % (prefix): name,
226            "%semail" % (prefix): email,
227            }
228        return result
229
230class DefaultObjectConverter(grok.Adapter):
231    """Turn string values into real values.
232
233    A converter can convert string values for objects that implement a
234    certain interface into real values based on the given interface.
235    """
236
237    grok.context(Interface)
238    grok.implements(IObjectConverter)
239
240    def __init__(self, iface):
241        self.iface = iface
242        # Omit known dictionaries since there is no widget available
243        # for dictionary schema fields
244        self.default_form_fields = form.Fields(iface).omit('description_dict')
245        return
246
247    def fromStringDict(self, data_dict, context, form_fields=None,
248                       mode='create'):
249        """Convert values in `data_dict`.
250
251        Converts data in `data_dict` into real values based on
252        `context` and `form_fields`.
253
254        `data_dict` is a mapping (dict) from field names to values
255        represented as strings.
256
257        The fields (keys) to convert can be given in optional
258        `form_fields`. If given, form_fields should be an instance of
259        :class:`zope.formlib.form.Fields`. Suitable instances are for
260        example created by :class:`grok.AutoFields`.
261
262        If no `form_fields` are given, a default is computed from the
263        associated interface.
264
265        The `context` can be an existing object (implementing the
266        associated interface) or a factory name. If it is a string, we
267        try to create an object using
268        :func:`zope.component.createObject`.
269
270        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
271        <DATA_DICT>)`` where
272
273        ``<FIELD_ERRORS>``
274           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
275           error that happened when validating the input data in
276           `data_dict`
277
278        ``<INVARIANT_ERRORS>``
279           is a list of invariant errors concerning several fields
280
281        ``<DATA_DICT>``
282           is a dict with the values from input dict converted.
283
284        If mode is ``'create'`` or ``'update'`` then some additional
285        filtering applies:
286
287        - values set to DELETION_MARKER are set to missing_value (or
288          default value if field is required) and
289
290        - values set to IGNORE_MARKER are ignored and thus not part of
291          the returned ``<DATA_DICT>``.
292
293        If errors happen, i.e. the error lists are not empty, always
294        an empty ``<DATA_DICT>`` is returned.
295
296        If ``<DATA_DICT>`` is non-empty, there were no errors.
297        """
298        if form_fields is None:
299            form_fields = self.default_form_fields
300
301        request = TestRequest(form={})
302        new_data = dict()
303        for key, val in data_dict.items():
304            val = val.strip()
305            field = form_fields.get(key, None)
306            if field is not None:
307                # let adapters to the respective schema fields do the
308                # further fake-request processing
309                schema_field = field.interface[field.__name__]
310                field_converter = IFieldConverter(schema_field)
311                if mode in ('update', 'create'):
312                    if val == IGNORE_MARKER:
313                        continue
314                    elif val == DELETION_MARKER:
315                        val = schema_field.missing_value
316                        if schema_field.required:
317                            val = schema_field.default
318                        new_data[key] = val
319                        continue
320                request.form.update(
321                    field_converter.request_data(
322                        key, val, schema_field, 'form.%s' % key)
323                    )
324            else:
325                request.form['form.%s' % key] = val
326
327        obj = context
328        if isinstance(context, basestring):
329            # If we log initialization transitions in the __init__
330            # method of objects, a second (misleading) log entry
331            # will be created here.
332            obj = createObject(context)
333
334        widgets = form.setUpInputWidgets(
335            form_fields, 'form', obj, request)
336
337        errors = getWidgetsData(widgets, 'form', new_data)
338
339        invariant_errors = form.checkInvariants(form_fields, new_data)
340
341        if errors or invariant_errors:
342            err_messages = [(key, err.args[0]) for key, err in errors]
343            invariant_errors = [err.message for err in invariant_errors]
344            return err_messages, invariant_errors, {}
345
346        return errors, invariant_errors, new_data
Note: See TracBrowser for help on using the repository browser.