Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

newforms-admin: Initial implementation of FormSet.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@4836 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit e2b49c379a56730b73d79ec05d6edc790ba08cc9 1 parent 132cf25
Joseph Kocherhans jkocherhans authored
18 django/newforms/forms.py
View
@@ -159,6 +159,24 @@ def non_field_errors(self):
"""
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.
154 django/newforms/formsets.py
View
@@ -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
152 tests/regressiontests/forms/tests.py
View
@@ -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)
Please sign in to comment.
Something went wrong with that request. Please try again.