root/trunk/newforms_extra/multiforms.py @ 99

Revision 99, 13.2 kB (checked in by akaihola, 3 years ago)

[newforms_extra] Heavy refactorization and development of newforms_extra.

Line 
1#!/usr/bin/python
2# -*- encoding: utf-8 -*-
3
4
5import re
6import new
7from itertools import chain
8
9from django import newforms as forms
10from django.db import models
11from django.utils.datastructures import MultiValueDict
12from django.utils.translation import gettext
13
14from ambidjangolib.newforms_extra.models import save_instance
15from ambidjangolib.newforms_extra.forms import Form
16
17
18class IntegerOrNewField(forms.IntegerField):
19
20    new_re = re.compile('NEW(\d+)$')
21
22    def clean(self, value):
23        if self.new_re.match(value):
24            return value
25        else:
26            return super(IntegerOrNewField, self).clean(value)
27
28
29class MultiForm(Form):
30    """
31    Many instances of the same model can be shown on and saved from a
32    single form.  Corresponding HTML fields on repeated forms have
33    equal names, and the ``MultiForm.forms_from_POST()`` method
34    creates multiple MultiForm instances based on that data.
35    """
36
37    core = {}
38    new_prefix = 'NEW'
39
40    @classmethod
41    def _model(cls, *args):
42        raise NotImplementedError(
43            'Subclasses of MultiForm must define _model.')
44
45
46    ###########################################################################
47    # Initialization
48    ###########################################################################
49
50    def __init__(self, *args, **kw):
51        super(MultiForm, self).__init__(*args, **kw)
52        for fieldname, field in self.fields.items():
53            field.widget.attrs['class'] = '%s-%s' % (
54                self.__class__.prefix, fieldname)
55        # Repeated forms need unique id="" attributes.  Generate them based on
56        # the id field value.
57        self.auto_id = 'id_%%s.%s' % self.get_field_val(self.pk_field)
58
59
60    ###########################################################################
61    # Methods for creating MultiForms from POST data
62    ###########################################################################
63
64    @classmethod
65    def forms_from_POST(cls, POST, *args, **kw):
66        """
67        Create a list of MultiForm objects for POST data.
68
69        >>> class TestForm(MultiForm):
70        ...     fld = forms.CharField()
71        ...     prefix = 'px'
72        >>> request_POST = {'px-fld': ['val1', 'val2']}
73        >>> [f.data for f in TestForm.forms_from_POST(request_POST)]
74        [{'px-fld': 'val1'}, {'px-fld': 'val2'}]
75        """
76        return cls._forms_from_data(POST, *args, **kw)
77
78
79    @classmethod
80    def save_forms_from_POST(cls, POST, *args, **kw):
81        """
82        Retrieve a list of MultiForm objects for POST data.  Try to
83        save each of the objects.  Return three lists: saved, deleted
84        and invalid forms.
85        """
86        saved = []
87        deleted = []
88        invalid = []
89        for form in cls.forms_from_POST(POST, *args, **kw):
90            if form.is_valid():
91                instance = form.save()
92                if instance is None:
93                    # object was deleted
94                    deleted.append(form)
95                else:
96                    # object was saved
97                    saved.append(cls.for_instance(instance))
98            else:
99                invalid.append(form)
100        return saved, deleted, invalid
101
102
103    @classmethod
104    def from_data(cls, data=None, index=None, initial=None):
105        """
106        Create one form instance and extract its data from POST field
107        arrays.  ``index`` is the index of this form among all
108        repeated forms for the same model.
109
110        Non-blank core values for required fields are regarded as
111        invalid.
112        """
113
114        if index is None:
115            raise ValueError('form index missing')
116
117        form = cls(
118            data=cls._extract_data(data, index),
119            initial=cls._extract_data(initial, index, prefix=False),
120            prefix=cls.prefix)
121        form.form_index = index
122
123        #
124        # The ID field must accept NEW* values for new objects
125        #
126        form._fix_pk_field_clean()
127
128        #
129        # Fields with core must regard values equal to the core as
130        # empty values
131        #
132        form._fix_fields_clean()
133
134        return form
135
136
137    ###########################################################################
138    # Saving data into DB based of form field values
139    ###########################################################################
140
141    def save(self, defaults=None):
142        """
143        Save form data into DB.  A new object is created and saved,
144        and existing objects are retrieved and updated.  An existing
145        object with its coreed fields left blank is deleted.
146        """
147        if self.is_new():
148            # create a new instance
149            instance = self._model(**defaults or {})
150        else:
151            # retrieve the old instance
152            instance = self._model.objects.get(
153                pk=self.get_field_val(self.pk_field))
154        if self.is_deleted():
155            # delete the object
156            instance.delete()
157            return None
158        else:
159            # use ambidjangolib.newforms_extra.models.save_instance to combine
160            # values from this form to old values from the instance and save the
161            # object
162            saved_instance = save_instance(self, instance)
163            # update ID from NEWx to real DB ID
164            fieldname, value = self._extract_instance_field(
165                saved_instance, self.pk_field)
166            self.data[fieldname] = value
167            return saved_instance
168
169
170    ###########################################################################
171    # New blank form handling
172    ###########################################################################
173
174    @classmethod
175    def make_new(cls, new_id, **initial_data):
176        """
177        Create a new blank form.
178        """
179        i = cls.core.copy()
180        i.update(initial_data)
181        i[cls.pk_field] = '%s%s' % (cls.new_prefix, new_id)
182        #data = dict((cls.add_prefix(fieldname), value)
183        #            for (fieldname, value) in initial_data.items())
184        return cls(initial=i)
185
186
187    def is_new(self):
188        """
189        Return True if the value of the form's ID field begins with
190        the new form prefix (default: 'NEW').
191        """
192        return self.get_new_id() is not None
193
194
195    def get_new_id(self):
196        """
197        Return the new form identifier number, e.g. the integer 1 for
198        a form whose pk_field's value is 'NEW1'.
199        """
200        value = self.get_field_val(self.pk_field)
201        if value.startswith(self.new_prefix):
202            return int(value[len(self.new_prefix):])
203        else:
204            return None
205
206
207    @staticmethod
208    def next_new_id(formlist):
209        return max(chain([0], (form.get_new_id() for form in formlist))) + 1
210
211
212    def non_empty_fields(self):
213        """
214        Return a dict of those coreed fields which are not are blank
215        or identical to their core.
216
217        THIS FUNCTION IS HERE FOR DEBUGGING PURPOSES
218        """
219        result = {}
220        for fieldname, field in self.fields.items():
221            if field.widget.is_hidden or fieldname not in self.core:
222                continue
223            value = self.get_field_val(fieldname)
224            if not value or value == self.core[fieldname]:
225                continue
226            result[fieldname] = value
227        return result
228
229
230    @classmethod
231    def _forms_from_data(cls, data, *args, **kw):
232        """
233        Create form objects for fields in data.
234
235        >>> class TestForm(MultiForm):
236        ...     fld1 = forms.CharField()
237        ...     fld2 = forms.CharField()
238        ...     prefix = 'px'
239
240        >>> testdata = {'px-fld1': ['val1', 'val2']}
241        >>> testinitial = {'fld2': ['val3', 'val4']}
242
243        >>> from pprint import pprint
244        >>> pprint([(f.data, f.initial)
245        ...         for f in TestForm._forms_from_data(
246        ...             data=testdata, initial=testinitial)])
247        [({'px-fld1': 'val1'}, {'fld2': 'val3'}),
248         ({'px-fld1': 'val2'}, {'fld2': 'val4'})]
249        """
250        forms = []
251        while True:
252            try:
253                forms.append(cls.from_data(data, len(forms), *args, **kw))
254            except IndexError:
255                break
256        return forms
257
258
259    ###########################################################################
260    # Internal validation customizations called from ``from_data``
261    ###########################################################################
262
263    #def clean(self):
264    #    """
265    #    If a form's coreed fields are cleared and the value of
266    #    ``pk_field`` identifies an instance in the DB, the instance
267    #    should be deleted and the form should pass validation.
268    #    """
269    #    if self.is_deleted():
270    #        # The form for an instance in the DB was cleared blank, so
271    #        # the instance should be deleted.  Clear any errors on the
272    #        # coreed fields so the form will pass validation.
273    #        for fieldname in self.core:
274    #            del self.__errors[fieldname]
275    #    return super(MultiForm, self).clean()
276
277
278    def _fix_pk_field_clean(self):
279        """
280        Replace the clean method of the id field so prefixed new form
281        IDs are accepted as valid.  This is called in ``from_data``.
282        """
283        def clean_pk_field(form, value):
284            if value and value.startswith(self.new_prefix):
285                return value
286            else:
287                return orig_id_clean(value)
288        pk_field = self.fields[self.pk_field]
289        orig_id_clean = pk_field.clean
290        pk_field.clean = new.instancemethod(clean_pk_field, pk_field)
291
292
293    def _fix_fields_clean(self):
294        """
295        Replace the clean methods of required and coreed fields so
296        core values are regarded as empty values.  This is called in
297        ``from_data``.
298        """
299        for fieldname, field in self.fields.items():
300            if field.required and fieldname in self.core:
301                def new_clean(field, value, fieldname=fieldname):
302                    if value == self.core[fieldname]:
303                        value = ''
304                    if self.is_deleted():
305                        return value
306                    else:
307                        return orig_clean(value)
308                orig_clean = field.clean
309                field.clean = new.instancemethod(new_clean, field)
310
311
312    ###########################################################################
313    # Internal helper for parsing POST array data
314    ###########################################################################
315
316    @classmethod
317    def _extract_data(cls, data, index, prefix=True):
318        """
319        >>> class TestForm(MultiForm):
320        ...     testfield = forms.CharField()
321        ...     prefix = 'testprefix'
322
323        >>> testdata = {'testfield': ['val1', 'val2']}
324
325        >>> print TestForm._extract_data(testdata, 0, prefix=False)
326        {'testfield': 'val1'}
327
328        >>> print TestForm._extract_data(testdata, 2, prefix=False)
329        Traceback (most recent call last):
330        IndexError: list index out of range
331
332        >>> testdata = {'testprefix-testfield': ['val1', 'val2']}
333
334        >>> print TestForm._extract_data(testdata, 1, prefix=True)
335        {'testprefix-testfield': 'val2'}
336
337        >>> print TestForm._extract_data(testdata, 2, prefix=True)
338        Traceback (most recent call last):
339        IndexError: list index out of range
340
341        >>> TestForm._extract_data({'dummy': '1'}, 0)
342        Traceback (most recent call last):
343        IndexError: No data for multiform
344        """
345        if data is None:
346            return None
347        if not isinstance(data, MultiValueDict):
348            data = MultiValueDict(data)
349        my_data = {}
350        for field_name in cls.base_fields.keys():
351            if prefix:
352                field_name = cls.add_prefix(field_name)
353            if field_name in data:
354                my_data[field_name] = data.getlist(field_name)[index]
355        if not my_data:
356            raise IndexError('No data for multiform')
357        return my_data
358
359
360    ###########################################################################
361    # Miscellaneous helpers
362    ###########################################################################
363
364    def __repr__(self): return "<%s %s>" % (
365        self.__class__.__name__,
366        self.get_field_val(self.pk_field))
367
368
369    def get_field_val(self, fieldname):
370        """
371        Return the value of the given field by plain field name.
372        Knows about the prefix and prepends it automatically.
373        """
374        full_fieldname = self.add_prefix(fieldname)
375        try:
376            return self.data[full_fieldname]
377        except KeyError:
378            return self.initial[fieldname]
379
380
381    def dump(self):
382        return '[%s %s]' % (
383            self.__class__.__name__,
384            self.get_field_val(self.pk_field))
385
386
387    ###########################################################################
388    # Trashbin -- are these methods used anywhere?
389    ###########################################################################
390
391    @classmethod
392    def forms_from_data(cls, data):
393        """
394        Create MultiForm objects for matching fields found in data.
395        """
396        return cls._forms_from_data(data=data)
397
398
399    @classmethod
400    def forms_from_initial(cls, initial):
401        """
402        Create MultiForm objects for matching fields found in initial data.
403        """
404        return cls._forms_from_data(None, initial=initial)
405
Note: See TracBrowser for help on using the browser.