~rvb/maas/parent-child-relationship

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# Copyright 2012 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Config forms utilities."""

from __future__ import (
    absolute_import,
    print_function,
    unicode_literals,
    )

str = None

__metaclass__ = type
__all__ = [
    'DictCharField',
    'DictCharWidget',
    'SKIP_CHECK_NAME',
    ]

from collections import OrderedDict

from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.forms.fields import Field
from django.forms.util import ErrorList
from django.utils.safestring import mark_safe


SKIP_CHECK_NAME = 'skip_check'


class DictCharField(forms.MultiValueField):
    """A field to edit a dictionary of strings.  Each entry in the
    dictionary corresponds to a sub-field.

    The field is constructed with a list of tuples containing the name of the
    sub-fields and the sub-field themselves.  An optional parameter
    'skip_check' allows the storing of an arbitrary dictionary in the field,
    bypassing any validation made by the sub-fields.

    For instance, if we create a form with a single DictCharField::

      >>> class ExampleForm(forms.Form):
      ...     example = DictCharField([
      ...             ('field1', forms.CharField(label="Field 1")),
      ...             ('field2', forms.CharField(label="Field 2")),
      ...         ])
      >>> from django.http import QueryDict
      >>> data = QueryDict('example_field1=subvalue1&example_field2=subvalue2')
      >>> form = ExampleForm(data)
      >>> # The 'cleaned_data' of the 'example' field is populated with the
      >>> # values of the subfields.
      >>> form.cleaned_data['example']
      {'field1': 'subvalue1', 'field2': 'subvalue2'}

    """

    def __init__(self, field_items, skip_check=False, *args,
                 **kwargs):
        self.field_dict = OrderedDict(field_items)
        self.skip_check = skip_check
        # Make sure no subfield is named 'SKIP_CHECK_NAME'. If
        # skip_check is True this field will clash with the addtional
        # subfield added by the DictCharField constructor.  We perform
        # this check even if skip_check=False because having a field named
        # 'skip_check' that isn't used to actually skip the checks would be
        # very confusing.
        if SKIP_CHECK_NAME in self.field_dict.keys():
            raise RuntimeError(
                "'%s' is a reserved name "
                "(it can't be used to name a subfield)." % SKIP_CHECK_NAME)
        # if skip_check: add a BooleanField to the list of fields, this will
        # be used to skip the validation of the fields and accept arbitrary
        # data.
        if skip_check:
            self.field_dict[SKIP_CHECK_NAME] = forms.BooleanField(
                required=False)
        self.names = [name for name in self.field_dict.keys()]
        # Create the DictCharWidget with init values from the list of fields.
        self.fields = self.field_dict.values()
        self.widget = DictCharWidget(
            [field.widget for field in self.fields],
            self.names,
            [field.initial for field in self.fields],
            [field.label for field in self.fields],
            skip_check=skip_check,
            )
        # Upcall to Field and not MultiValueField to avoid setting all the
        # subfields' 'required' attributes to False.
        Field.__init__(self, *args, **kwargs)

    def compress(self, data):
        """Returns a single value for the given list of values."""
        if data:
            if isinstance(data, dict):
                # If the data is a dict, this means that we're in the
                # situation where skip_check was true and we simply
                # return the dict.
                return data
            else:
                # Here data is the list of the values of the subfields,
                # return a dict with all the right keys:
                # For instance, for a DictCharField created with two
                # subfields 'field1' and 'field2', data will be
                # ['value1', 'value2'] and we will return:
                # {'field1': 'value1', 'field2': 'value2'}
                return dict(zip(self.names, data))
        return None

    def get_names(self):
        if self.skip_check:
            return self.names[:-1]
        else:
            return self.names

    def clean(self, value):
        """Validates every value in the given list. A value is validated
        against the corresponding Field in self.fields.

        This is an adapted version of Django's MultiValueField_ clean method.

        The differences are:
        - the method is split into clean_global_empty and
             clean_sub_fields;
        - the field and value corresponding to the SKIP_CHECK_NAME boolean
            field are removed;
        - each individual field 'required' attribute is used instead of the
            DictCharField's 'required' attribute.  This allows a more
            fine-grained control of what's required and what's not required.

        .. _MultiValueField: http://code.djangoproject.com/
            svn/django/tags/releases/1.3.1/django/forms/fields.py
        """
        skip_check = (
            self.skip_check and
            self.widget.widgets[-1].value_from_datadict(
                value, files=None, name=SKIP_CHECK_NAME))
        # Remove the 'skip_check' value from the list of values.
        try:
            value.pop(SKIP_CHECK_NAME)
        except KeyError:
            pass
        if skip_check:
            # If the skip_check option is on and the value of the boolean
            # field is true: don't perform any validation and simply return
            # the dictionary.
            return value
        else:
            self.clean_unknown_params(value)
            values = [value.get(name) for name in self.get_names()]
            result = self.clean_global_empty(values)
            if result is None:
                return None
            else:
                return self.clean_sub_fields(values)

    def clean_unknown_params(self, value):
        unknown_params = set(value.keys()).difference(self.get_names())
        if len(unknown_params) != 0:
            raise ValidationError(
                "Unknown parameter(s): %s." % ', '.join(unknown_params))

    def clean_global_empty(self, value):
        """Make sure the value is not empty and is thus suitable to be
        feed to the sub fields' validators."""
        if not value or isinstance(value, (list, tuple)):
            # value is considered empty if it is in
            # validators.EMPTY_VALUES, or if each of the subvalues is
            # None.
            is_empty = (
                value in validators.EMPTY_VALUES or
                len(filter(lambda x: x is not None, value)) == 0)
            if is_empty:
                if self.required:
                    raise ValidationError(self.error_messages['required'])
                else:
                    return None
            else:
                return True
        else:
            raise ValidationError(self.error_messages['invalid'])

    def clean_sub_fields(self, value):
        """'value' being the list of the values of the subfields, validate
        each subfield."""
        clean_data = []
        errors = ErrorList()
        # Remove the field corresponding to the SKIP_CHECK_NAME boolean field
        # if required.
        fields = self.fields if not self.skip_check else self.fields[:-1]
        for index, field in enumerate(fields):
            try:
                field_value = value[index]
            except IndexError:
                field_value = None
            # Check the field's 'required' field instead of the global
            # 'required' field to allow subfields to be required or not.
            if field.required and field_value in validators.EMPTY_VALUES:
                errors.append(
                    '%s: %s' % (field.label, self.error_messages['required']))
                continue
            try:
                clean_data.append(field.clean(field_value))
            except ValidationError, e:
                # Collect all validation errors in a single list, which we'll
                # raise at the end of clean(), rather than raising a single
                # exception for the first error we encounter.
                errors.extend(
                    '%s: %s' % (field.label, message)
                    for message in e.messages)
        if errors:
            raise ValidationError(errors)

        out = self.compress(clean_data)
        self.validate(out)
        return out


def get_all_prefixed_values(data, name):
    """From a dictionary, extract a sub-dictionary of all the keys/values for
    which the key starts with a particular prefix.  In the resulting
    dictionary, strip the prefix from the keys::

      >>> get_all_prefixed_values(
      ...     {'prefix_test': 'a', 'key': 'b'}, 'prefix_')
      {'test': 'a'}

    """
    result = {}
    for key, value in data.items():
        if key.startswith(name):
            new_key = key[len(name):]
            result[new_key] = value
    return result


class DictCharWidget(forms.widgets.MultiWidget):
    """A widget to display the content of a dictionary.  Each key will
    correspond to a subwidget.  Although there is no harm in using this class
    directly, note that this is mostly destined to be used internally
    by DictCharField.

    The customization compared to Django's MultiWidget_ are:
    - DictCharWidget displays all the subwidgets inside a fieldset tag;
    - DictCharWidget displays a label for each subwidget;
    - DictCharWidget names each subwidget 'main_widget_sub_widget_name'
        instead of 'main_widget_0';
    - DictCharWidget has the (optional) ability to skip all the validation
        and instead fetch all the values prefixed by 'main_widget_' in the
        input data.

    To achieve that, we customize:
    - 'render' which returns the HTML code to display this widget;
    - 'id_for_label' which return the HTML ID attribute for this widget
        for use by a label.  This widget is composed of multiple widgets so
        the id of the first widget is used;
    - 'value_from_datadict' which fetches the value of the data to be
        processed by this form to give a 'data' dictionary.  We need to
        customize that because we've changed the way MultiWidget names
        sub-widgets;
    - 'decompress' which takes a single "compressed" value and returns a list
        of values to be used by the widgets.

    .. _MultiWidget: http://code.djangoproject.com/
        svn/django/tags/releases/1.3.1/django/forms/widgets.py

    Arguments:
    widgets -- list of widgets for sub-fields.
    names -- list of names for sub-fields.
    initials -- list of initial values for sub-fields.
    labels -- list of labels for sub-fields

    Keyword arguments:
    skip_check -- boolean indicating validation will be skipped.
    attrs -- see Widget.attrs
    """

    def __init__(self, widgets, names,
                 initials, labels, skip_check=False, attrs=None):
        self.names = names
        self.initials = initials
        self.labels = labels
        self.skip_check = skip_check
        super(DictCharWidget, self).__init__(widgets, attrs)

    def render(self, name, value, attrs=None):
        # value is a list of values, each corresponding to a widget
        # in self.widgets.
        # Do not display the 'skip_check' boolean widget.
        if self.skip_check:
            widgets = self.widgets[:-1]
        else:
            widgets = self.widgets
        if not isinstance(value, list):
            value = self.decompress(value)
        if len(widgets) == 0:
            return mark_safe(self.format_output(''))

        output = ['<fieldset>']
        final_attrs = self.build_attrs(attrs)
        id_ = final_attrs.get('id', None)

        for index, widget in enumerate(widgets):
            try:
                widget_value = value[index]
            except IndexError:
                try:
                    widget_value = self.initials[index]
                except IndexError:
                    widget_value = None
            if id_:
                final_attrs = dict(
                    final_attrs, id='%s_%s' % (id_, self.names[index]))
            # Add label to each sub-field.
            if id_:
                label_for = ' for="%s"' % final_attrs['id']
            else:
                label_for = ''
            output.append(
                '<label%s>%s</label>' % (
                    label_for, self.labels[index]))
            output.append(
                widget.render(
                    '%s_%s' % (name, self.names[index]), widget_value,
                    final_attrs))
        output.append('</fieldset>')
        return mark_safe(self.format_output(output))

    def id_for_label(self, id_):
        """Returns the HTML ID attribute of this Widget.  Since this is a
        widget with multiple HTML elements, this method returns an ID
        corresponding to the first ID in the widget's tags."""
        # See the comment for RadioSelect.id_for_label()
        if id_:
            id_ += '_%s' % self.names[0]
        return id_

    def value_from_datadict(self, data, files, name):
        """Extract the values for this widget from a data dict (QueryDict).
        :param data: The data dict (usually request.data or request.GET where
            request is a django.http.HttpRequest).
        :type data: dict
        :param files: The files dict (usually request.FILES where request is a
            django.http.HttpRequest).
        :type files: dict
        :param name: The name of the widget.
        :type name: unicode
        :return: The extracted values as a dictionary.
        :rtype: dict or list
        """
        return get_all_prefixed_values(data, name + '_')

    def decompress(self, value):
        """Returns a list of decompressed values for the given compressed
        value.  The given value can be assumed to be valid, but not
        necessarily non-empty."""
        if value not in validators.EMPTY_VALUES:
            return [value.get(name, None) for name in self.names]
        else:
            return [None] * len(self.names)