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

Last change on this file since 17805 was 17787, checked in by Henrik Bettermann, 7 months ago

Add SessionConfigurationProcessor.
Add ConfigurationContainerProcessor.
Add ConfigurationContainerExporter.

  • Property svn:keywords set to Id
File size: 12.4 KB
Line 
1## $Id: converters.py 17787 2024-05-15 06:42:58Z 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(
245            'description_dict', 'frontpage_dict')
246        return
247
248    def fromStringDict(self, data_dict, context, form_fields=None,
249                       mode='create'):
250        """Convert values in `data_dict`.
251
252        Converts data in `data_dict` into real values based on
253        `context` and `form_fields`.
254
255        `data_dict` is a mapping (dict) from field names to values
256        represented as strings.
257
258        The fields (keys) to convert can be given in optional
259        `form_fields`. If given, form_fields should be an instance of
260        :class:`zope.formlib.form.Fields`. Suitable instances are for
261        example created by :class:`grok.AutoFields`.
262
263        If no `form_fields` are given, a default is computed from the
264        associated interface.
265
266        The `context` can be an existing object (implementing the
267        associated interface) or a factory name. If it is a string, we
268        try to create an object using
269        :func:`zope.component.createObject`.
270
271        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
272        <DATA_DICT>)`` where
273
274        ``<FIELD_ERRORS>``
275           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
276           error that happened when validating the input data in
277           `data_dict`
278
279        ``<INVARIANT_ERRORS>``
280           is a list of invariant errors concerning several fields
281
282        ``<DATA_DICT>``
283           is a dict with the values from input dict converted.
284
285        If mode is ``'create'`` or ``'update'`` then some additional
286        filtering applies:
287
288        - values set to DELETION_MARKER are set to missing_value (or
289          default value if field is required) and
290
291        - values set to IGNORE_MARKER are ignored and thus not part of
292          the returned ``<DATA_DICT>``.
293
294        If errors happen, i.e. the error lists are not empty, always
295        an empty ``<DATA_DICT>`` is returned.
296
297        If ``<DATA_DICT>`` is non-empty, there were no errors.
298        """
299        if form_fields is None:
300            form_fields = self.default_form_fields
301
302        request = TestRequest(form={})
303        new_data = dict()
304        for key, val in data_dict.items():
305            val = val.strip()
306            field = form_fields.get(key, None)
307            if field is not None:
308                # let adapters to the respective schema fields do the
309                # further fake-request processing
310                schema_field = field.interface[field.__name__]
311                field_converter = IFieldConverter(schema_field)
312                if mode in ('update', 'create'):
313                    if val == IGNORE_MARKER:
314                        continue
315                    elif val == DELETION_MARKER:
316                        val = schema_field.missing_value
317                        if schema_field.required:
318                            val = schema_field.default
319                        new_data[key] = val
320                        continue
321                request.form.update(
322                    field_converter.request_data(
323                        key, val, schema_field, 'form.%s' % key)
324                    )
325            else:
326                request.form['form.%s' % key] = val
327
328        obj = context
329        if isinstance(context, basestring):
330            # If we log initialization transitions in the __init__
331            # method of objects, a second (misleading) log entry
332            # will be created here.
333            obj = createObject(context)
334
335        widgets = form.setUpInputWidgets(
336            form_fields, 'form', obj, request)
337
338        errors = getWidgetsData(widgets, 'form', new_data)
339
340        invariant_errors = form.checkInvariants(form_fields, new_data)
341
342        if errors or invariant_errors:
343            err_messages = [(key, err.args[0]) for key, err in errors]
344            invariant_errors = [err.message for err in invariant_errors]
345            return err_messages, invariant_errors, {}
346
347        return errors, invariant_errors, new_data
Note: See TracBrowser for help on using the repository browser.