Author: jkocherhans
Date: 2007-03-27 23:31:06 -0500 (Tue, 27 Mar 2007)
New Revision: 4836

Added:
   django/branches/newforms-admin/django/newforms/formsets.py
Modified:
   django/branches/newforms-admin/django/newforms/forms.py
   django/branches/newforms-admin/tests/regressiontests/forms/tests.py
Log:
[newforms-admin] Initial implementation of FormSet.

Modified: django/branches/newforms-admin/django/newforms/forms.py
===================================================================
--- django/branches/newforms-admin/django/newforms/forms.py     2007-03-27 
22:35:22 UTC (rev 4835)
+++ django/branches/newforms-admin/django/newforms/forms.py     2007-03-28 
04:31:06 UTC (rev 4836)
@@ -159,6 +159,24 @@
         """
         return self.errors.get(NON_FIELD_ERRORS, ErrorList())
 
+    def is_empty(self, exceptions=None):
+        """
+        Returns True if this form has been bound and all fields that aren't
+        listed in exceptions are empty.
+        """
+        # TODO: This could probably use some optimization
+        exceptions = exceptions or []
+        for name, field in self.fields.items():
+            if name in exceptions:
+                continue
+            # value_from_datadict() gets the data from the dictionary.
+            # Each widget type knows how to retrieve its own data, because some
+            # widgets split data over several HTML fields.
+            value = field.widget.value_from_datadict(self.data, 
self.add_prefix(name))
+            if value not in (None, ''):
+                return False
+        return True
+
     def full_clean(self):
         """
         Cleans all of self.data and populates self.__errors and 
self.clean_data.

Added: django/branches/newforms-admin/django/newforms/formsets.py
===================================================================
--- django/branches/newforms-admin/django/newforms/formsets.py                  
        (rev 0)
+++ django/branches/newforms-admin/django/newforms/formsets.py  2007-03-28 
04:31:06 UTC (rev 4836)
@@ -0,0 +1,154 @@
+from django import newforms as forms
+
+# special field names
+FORM_COUNT_FIELD_NAME = 'COUNT'
+ORDERING_FIELD_NAME = 'ORDER'
+DELETION_FIELD_NAME = 'DELETE'
+
+class ManagementForm(forms.Form):
+    """
+    ``ManagementForm`` is used to keep track of how many form instances
+    are displayed on the page. If adding new forms via javascript, you should
+    increment the count field of this form as well.
+    """
+    def __init__(self, *args, **kwargs):
+        self.base_fields[FORM_COUNT_FIELD_NAME] = 
forms.IntegerField(widget=forms.HiddenInput)
+        super(ManagementForm, self).__init__(*args, **kwargs)
+
+class FormSet(object):
+    """A collection of instances of the same Form class."""
+
+    def __init__(self, form_class, data=None, auto_id='id_%s', prefix=None, 
initial=None):
+        self.form_class = form_class
+        self.prefix = prefix or 'form'
+        self.auto_id = auto_id
+        # initialization is different depending on whether we recieved data, 
initial, or nothing
+        if data:
+            self.management_form = ManagementForm(data, auto_id=self.auto_id, 
prefix=self.prefix)
+            if self.management_form.is_valid():
+                form_count = 
self.management_form.clean_data[FORM_COUNT_FIELD_NAME]
+            else:
+                # not sure that ValidationError is the best thing to raise here
+                raise forms.ValidationError('ManagementForm data is missing or 
has been tampered with')
+            self.form_list = self._forms_for_data(data, form_count=form_count)
+        elif initial:
+            form_count = len(initial)
+            self.management_form = 
ManagementForm(initial={FORM_COUNT_FIELD_NAME: form_count+1}, 
auto_id=self.auto_id, prefix=self.prefix)
+            self.form_list = self._forms_for_initial(initial, 
form_count=form_count)
+        else:
+            self.management_form = 
ManagementForm(initial={FORM_COUNT_FIELD_NAME: 1}, auto_id=self.auto_id, 
prefix=self.prefix)
+            self.form_list = self._empty_forms(form_count=1)
+
+    # TODO: initialization needs some cleanup and some restructuring
+    # TODO: allow more than 1 extra blank form to be displayed
+
+    def _forms_for_data(self, data, form_count):
+        form_list = []
+        for i in range(0, form_count-1):
+            form_instance = self.form_class(data, auto_id=self.auto_id, 
prefix=self.add_prefix(i))
+            self.add_fields(form_instance, i)
+            form_list.append(form_instance)
+        # hackish, but if the last form stayed empty, replace it with a 
+        # blank one. no 'data' or 'initial' arguments
+        form_instance = self.form_class(data, auto_id=self.auto_id, 
prefix=self.add_prefix(form_count-1))
+        if form_instance.is_empty():
+            form_instance = self.form_class(auto_id=self.auto_id, 
prefix=self.add_prefix(form_count-1))
+        self.add_fields(form_instance, form_count-1)
+        form_list.append(form_instance)
+        return form_list
+
+    def _forms_for_initial(self, initial, form_count):
+        form_list = []
+        # generate a form for each item in initial, plus one empty one
+        for i in range(0, form_count):
+            form_instance = self.form_class(initial=initial[i], 
auto_id=self.auto_id, prefix=self.add_prefix(i))
+            self.add_fields(form_instance, i)
+            form_list.append(form_instance)
+        # add 1 empty form
+        form_instance = self.form_class(auto_id=self.auto_id, 
prefix=self.add_prefix(i+1))
+        self.add_fields(form_instance, i+1)
+        form_list.append(form_instance)
+        return form_list
+
+    def _empty_forms(self, form_count):
+        form_list = []
+        # we only need one form, there's no inital data and no post data
+        form_instance = self.form_class(auto_id=self.auto_id, 
prefix=self.add_prefix(0))
+        form_list.append(form_instance)
+        return form_list
+
+    def get_forms(self):
+        return self.form_list
+
+    def add_fields(self, form, index):
+        """A hook for adding extra fields on to each form instance."""
+        pass
+
+    def add_prefix(self, index):
+        return '%s-%s' % (self.prefix, index)
+
+    def _get_clean_data(self):
+        return self.get_clean_data()
+
+    def get_clean_data(self):
+        clean_data_list = []
+        for form in self.get_non_empty_forms():
+            clean_data_list.append(form.clean_data)
+        return clean_data_list
+
+    clean_data = property(_get_clean_data)
+
+    def is_valid(self):
+        for form in self.get_non_empty_forms():
+            if not form.is_valid():
+                return False
+        return True
+
+    def get_non_empty_forms(self):
+        """Return all forms that aren't empty."""
+        return [form for form in self.form_list if not form.is_empty()]
+
+class FormSetWithDeletion(FormSet):
+    """A ``FormSet`` that handles deletion of forms."""
+
+    def add_fields(self, form, index):
+        """Add a delete checkbox to each form."""
+        form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', 
required=False)
+
+    def get_clean_data(self):
+        self.deleted_data = []
+        clean_data_list = []
+        for form in self.get_non_empty_forms():
+            if form.clean_data[DELETION_FIELD_NAME]:
+                # stick data marked for deletetion in self.deleted_data
+                self.deleted_data.append(form.clean_data)
+            else:
+               clean_data_list.append(form.clean_data)
+        return clean_data_list
+
+class FormSetWithOrdering(FormSet):
+    """A ``FormSet`` that handles re-ordering of forms."""
+
+    def get_non_empty_forms(self):
+        return [form for form in self.form_list if not 
form.is_empty(exceptions=[ORDERING_FIELD_NAME])]
+
+    def add_fields(self, form, index):
+        """Add an ordering field to each form."""
+        form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', 
initial=index+1)
+
+    def get_clean_data(self):
+        clean_data_list = []
+        for form in self.get_non_empty_forms():
+            clean_data_list.append(form.clean_data)
+        # sort clean_data by the 'ORDER' field
+        clean_data_list.sort(lambda x,y: x[ORDERING_FIELD_NAME] - 
y[ORDERING_FIELD_NAME])
+        return clean_data_list
+
+    def is_valid(self):
+        for form in self.get_non_empty_forms():
+            if not form.is_valid():
+                return False
+        return True
+
+# TODO: handle deletion and ordering in the same FormSet
+# TODO: model integration: form_for_instance and form_for_model type functions

Modified: django/branches/newforms-admin/tests/regressiontests/forms/tests.py
===================================================================
--- django/branches/newforms-admin/tests/regressiontests/forms/tests.py 
2007-03-27 22:35:22 UTC (rev 4835)
+++ django/branches/newforms-admin/tests/regressiontests/forms/tests.py 
2007-03-28 04:31:06 UTC (rev 4836)
@@ -2895,6 +2895,158 @@
 >>> p.clean_data
 {'first_name': u'John', 'last_name': u'Lennon', 'birthday': 
datetime.date(1940, 10, 9)}
 
+# FormSets ####################################################################
+
+FormSets allow you to create a bunch of instances of the same form class and
+get back clean data as a list of dicts.
+
+>>> from django.newforms import formsets
+
+>>> class ChoiceForm(Form):
+...     choice = CharField()
+...     votes = IntegerField()
+
+
+Create an empty form set
+
+>>> form_set = formsets.FormSet(ChoiceForm, prefix='choices', auto_id=False)
+>>> for form in form_set.get_forms():
+...     print form.as_ul()
+<li>Choice: <input type="text" name="choices-0-choice" /></li>
+<li>Votes: <input type="text" name="choices-0-votes" /></li>
+
+
+Forms pre-filled with initial data.
+
+>>> initial_data = [
+...     {'votes': 50, 'choice': u'The Doors', 'id': u'0'},
+...     {'votes': 51, 'choice': u'The Beatles', 'id': u'1'},
+... ]
+
+>>> form_set = formsets.FormSet(ChoiceForm, initial=initial_data, 
auto_id=False, prefix='choices')
+>>> print form_set.management_form.as_ul()
+<input type="hidden" name="choices-COUNT" value="3" />
+
+>>> for form in form_set.get_forms(): # print pre-filled forms
+...     print form.as_ul()
+<li>Choice: <input type="text" name="choices-0-choice" value="The Doors" 
/></li>
+<li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>
+<li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" 
/></li>
+<li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
+<li>Votes: <input type="text" name="choices-2-votes" /></li>
+
+
+Tests for dealing with POSTed data
+
+>>> data = {
+...     'choices-COUNT': u'3', # the number of forms rendered
+...     'choices-0-choice': u'The Doors',
+...     'choices-0-votes': u'50',
+...     'choices-1-choice': u'The Beatles',
+...     'choices-1-votes': u'51',
+...     'choices-2-choice': u'',
+...     'choices-2-votes': u'',
+... }
+
+
+>>> form_set = formsets.FormSet(ChoiceForm, data, auto_id=False, 
prefix='choices')
+>>> print form_set.is_valid()
+True
+>>> for data in form_set.clean_data:
+...     print data
+{'votes': 50, 'choice': u'The Doors'}
+{'votes': 51, 'choice': u'The Beatles'}
+
+
+FormSet with deletion fields
+
+>>> form_set = formsets.FormSetWithDeletion(ChoiceForm, initial=initial_data, 
auto_id=False, prefix='choices')
+>>> for form in form_set.get_forms(): # print pre-filled forms
+...     print form.as_ul()
+<li>Choice: <input type="text" name="choices-0-choice" value="The Doors" 
/></li>
+<li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>
+<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
+<li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" 
/></li>
+<li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>
+<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
+<li>Votes: <input type="text" name="choices-2-votes" /></li>
+<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
+
+>>> data = {
+...     'choices-COUNT': u'3', # the number of forms rendered
+...     'choices-0-choice': u'Fergie',
+...     'choices-0-votes': u'1000',
+...     'choices-0-DELETE': u'on', # Delete this choice.
+...     'choices-1-choice': u'The Decemberists',
+...     'choices-1-votes': u'150',
+...     'choices-2-choice': u'Calexico',
+...     'choices-2-votes': u'90',
+... }
+
+>>> form_set = formsets.FormSetWithDeletion(ChoiceForm, data, auto_id=False, 
prefix='choices')
+>>> print form_set.is_valid()
+True
+
+When we access form_set.clean_data, items marked for deletion won't be there,
+but they *will* be in form_set.deleted_data
+
+>>> for data in form_set.clean_data:
+...     print data
+{'votes': 150, 'DELETE': False, 'choice': u'The Decemberists'}
+{'votes': 90, 'DELETE': False, 'choice': u'Calexico'}
+
+>>> for data in form_set.deleted_data:
+...     print data
+{'votes': 1000, 'DELETE': True, 'choice': u'Fergie'}
+
+
+FormSet with Ordering
+
+>>> form_set = formsets.FormSetWithOrdering(ChoiceForm, initial=initial_data, 
auto_id=False, prefix='choices')
+>>> for form in form_set.get_forms(): # print pre-filled forms
+...     print form.as_ul()
+<li>Choice: <input type="text" name="choices-0-choice" value="The Doors" 
/></li>
+<li>Votes: <input type="text" name="choices-0-votes" value="50" /></li>
+<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
+<li>Choice: <input type="text" name="choices-1-choice" value="The Beatles" 
/></li>
+<li>Votes: <input type="text" name="choices-1-votes" value="51" /></li>
+<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
+<li>Votes: <input type="text" name="choices-2-votes" /></li>
+<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>
+
+>>> data = {
+...     'choices-COUNT': u'4', # the number of forms rendered
+...     'choices-0-choice': u'Fergie',
+...     'choices-0-votes': u'1000',
+...     'choices-0-ORDER': u'3',
+...     'choices-1-choice': u'The Decemberists',
+...     'choices-1-votes': u'150',
+...     'choices-1-ORDER': u'1',
+...     'choices-2-choice': u'Calexico',
+...     'choices-2-votes': u'90',
+...     'choices-2-ORDER': u'2',
+...     'choices-3-choice': u'',
+...     'choices-3-votes': u'',
+...     'choices-3-ORDER': u'4',
+... }
+
+>>> form_set = formsets.FormSetWithOrdering(ChoiceForm, data, auto_id=False, 
prefix='choices')
+>>> print form_set.is_valid()
+True
+
+The form_set.clean_data will be in the correct order as specified by the
+ORDER field from each form.
+
+>>> for data in form_set.clean_data:
+...     print data
+{'votes': 150, 'ORDER': 1, 'choice': u'The Decemberists'}
+{'votes': 90, 'ORDER': 2, 'choice': u'Calexico'}
+{'votes': 1000, 'ORDER': 3, 'choice': u'Fergie'}
+
+
 # Forms with NullBooleanFields ################################################
 
 NullBooleanField is a bit of a special case because its presentation (widget)


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to