diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py index c2649640a0..330fb088b0 100644 --- a/django/newforms/formsets.py +++ b/django/newforms/formsets.py @@ -5,6 +5,11 @@ FORM_COUNT_FIELD_NAME = 'COUNT' ORDERING_FIELD_NAME = 'ORDER' DELETION_FIELD_NAME = 'DELETE' +def formset_for_form(form, num_extra=1, orderable=False, deletable=False): + """Return a FormSet for the given form class.""" + attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable} + return type(form.__name__ + 'FormSet', (BaseFormSet,), attrs) + class ManagementForm(forms.Form): """ ``ManagementForm`` is used to keep track of how many form instances @@ -15,140 +20,130 @@ class ManagementForm(forms.Form): self.base_fields[FORM_COUNT_FIELD_NAME] = forms.IntegerField(widget=forms.HiddenInput) super(ManagementForm, self).__init__(*args, **kwargs) -class FormSet(object): +class BaseFormSet(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 + def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None): + self.is_bound = data is not None self.prefix = prefix or 'form' self.auto_id = auto_id + self.data = data + self.initial = initial # 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] + self.total_forms = self.management_form.clean_data[FORM_COUNT_FIELD_NAME] + self.required_forms = self.total_forms - self.num_extra 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) + self.required_forms = len(initial) + self.total_forms = self.required_forms + self.num_extra + self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 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) + self.required_forms = 0 + self.total_forms = self.num_extra + self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) - # TODO: initialization needs some cleanup and some restructuring - # TODO: allow more than 1 extra blank form to be displayed + def _get_form_list(self): + """Return a list of Form instances.""" + if not hasattr(self, '_form_list'): + self._form_list = [] + for i in range(0, self.total_forms): + kwargs = {'data': self.data, 'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} + if self.initial and i < self.required_forms: + kwargs['initial'] = self.initial[i] + form_instance = self.form_class(**kwargs) + # HACK: if the form was not completed, replace it with a blank one + if self.data and i >= self.required_forms and form_instance.is_empty(): + form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i)) + self.add_fields(form_instance, i) + self._form_list.append(form_instance) + return self._form_list - 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 + form_list = property(_get_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 full_clean(self): + """ + Cleans all of self.data and populates self.__errors and self.clean_data. + """ + is_valid = True + + errors = [] + if not self.is_bound: # Stop further processing. + self.__errors = errors + return + clean_data = [] + deleted_data = [] + + self._form_list = [] + # step backwards through the forms so when we hit the first filled one + # we can easily require the rest without backtracking + required = False + for i in range(self.total_forms-1, -1, -1): + kwargs = {'data': self.data, 'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} + + # prep initial data if there is some + if self.initial and i < self.required_forms: + kwargs['initial'] = self.initial[i] + + # create the form instance + form = self.form_class(**kwargs) + self.add_fields(form, i) + + if self.data and (i < self.required_forms or not form.is_empty(exceptions=[ORDERING_FIELD_NAME])): + required = True # forms cannot be empty anymore + + # HACK: if the form is empty and not required, replace it with a blank one + # this is necessary to keep form.errors empty + if not required and self.data and form.is_empty(exceptions=[ORDERING_FIELD_NAME]): + form = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i)) + self.add_fields(form, i) + else: + # if the formset is still vaild overall and this form instance + # is valid, keep appending to clean_data + if is_valid and form.is_valid(): + if self.deletable and form.clean_data[DELETION_FIELD_NAME]: + deleted_data.append(form.clean_data) + else: + clean_data.append(form.clean_data) + else: + is_valid = False + # append to errors regardless + errors.append(form.errors) + self._form_list.append(form) - 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 + deleted_data.reverse() + if self.orderable: + clean_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) + else: + clean_data.reverse() + errors.reverse() + self._form_list.reverse() + + if is_valid: + self.clean_data = clean_data + self.deleted_data = deleted_data + self.errors = errors + self._is_valid = is_valid + # TODO: user defined formset validation. + def add_fields(self, form, index): """A hook for adding extra fields on to each form instance.""" - pass + if self.orderable: + form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', initial=index+1) + if self.deletable: + form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', required=False) 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 + self.full_clean() + return self._is_valid # TODO: handle deletion and ordering in the same FormSet # TODO: model integration: form_for_instance and form_for_model type functions diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py new file mode 100644 index 0000000000..8d0e3b8d7c --- /dev/null +++ b/tests/regressiontests/forms/formsets.py @@ -0,0 +1,419 @@ +# -*- coding: utf-8 -*- +formset_tests = """ +# Basic FormSet creation and usage ############################################ + +FormSet allows us to use multiple instance of the same form on 1 page. For now, +the best way to create a FormSet is by using the formset_for_form function. + +>>> from django.newforms import Form, CharField, IntegerField +>>> from django.newforms.formsets import formset_for_form + +>>> class Choice(Form): +... choice = CharField() +... votes = IntegerField() + +>>> ChoiceFormSet = formset_for_form(Choice) + + +A FormSet constructor takes the same arguments as Form. Let's create a FormSet +for adding data. By default, it displays 1 blank form. It can display more, +but we'll look at how to do so later. + +>>> formset = ChoiceFormSet(auto_id=False, prefix='choices') +>>> for form in formset.form_list: +... print form.as_ul() +
  • Choice:
  • +
  • Votes:
  • + +On thing to note is that there needs to be a special value in the data. This +value tells the FormSet how many forms were displayed so it can tell how +many forms it needs to clean and validate. You could use javascript to create +new forms on the client side, but they won't get validated unless you increment +the COUNT field appropriately. + +>>> data = { +... 'choices-COUNT': '1', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... } + +We treat FormSet pretty much like we would treat a normal Form. FormSet has an +is_valid method, and a clean_data or errors attribute depending on whether all +the forms passed validation. However, unlike a Form instance, clean_data and +errors will be a list of dicts rather than just a single dict. + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> formset.clean_data +[{'votes': 100, 'choice': u'Calexico'}] + + +FormSet instances can also have an error attribute if validation failed for +any of the forms. + +>>> data = { +... 'choices-COUNT': '1', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{'votes': [u'This field is required.']}] + +Like a Form instance, clean_data won't exist if the formset wasn't validated. + +>>> formset.clean_data +Traceback (most recent call last): +... +AttributeError: 'ChoiceFormSet' object has no attribute 'clean_data' + + +We can also prefill a FormSet with existing data by providing an ``initial`` +argument to the constructor. ``initial`` should be a list of dicts. By default, +an extra blank form is included. + +>>> initial = [{'choice': u'Calexico', 'votes': 100}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.form_list: +... print form.as_ul() +
  • Choice:
  • +
  • Votes:
  • +
  • Choice:
  • +
  • Votes:
  • + + +Let's simulate what would happen if we submitted this form. + +>>> data = { +... 'choices-COUNT': '2', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> formset.clean_data +[{'votes': 100, 'choice': u'Calexico'}] + +But the second form was blank! Shouldn't we get some errors? No. If we display +a form as blank, it's ok for it to be submitted as blank. If we fill out even +one of the fields of a blank form though, it will be validated. We may want to +required that at least x number of forms are completed, but we'll show how to +handle that later. + +>>> data = { +... 'choices-COUNT': '2', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': 'The Decemberists', +... 'choices-1-votes': '', # missing value +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{}, {'votes': [u'This field is required.']}] + + +If we delete data that was pre-filled, we should get an error. Simply removing +data from form fields isn't the proper way to delete it. We'll see how to +handle that case later. + +>>> data = { +... 'choices-COUNT': '2', # the number of forms rendered +... 'choices-0-choice': '', # deleted value +... 'choices-0-votes': '', # deleted value +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}] + + +# Displaying more than 1 blank form ########################################### + +We can also display more than 1 empty form at a time. To do so, pass a +num_extra argument to formset_for_form. + +>>> ChoiceFormSet = formset_for_form(Choice, num_extra=3) + +>>> formset = ChoiceFormSet(auto_id=False, prefix='choices') +>>> for form in formset.form_list: +... print form.as_ul() +
  • Choice:
  • +
  • Votes:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Choice:
  • +
  • Votes:
  • + +Since we displayed every form as blank, we will also accept them back as blank. +This may seem a little strange, but later we will show how to require a minimum +number of forms to be completed. + +>>> data = { +... 'choices-COUNT': '3', # the number of forms rendered +... 'choices-0-choice': '', +... 'choices-0-votes': '', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> formset.clean_data +[] + + +We can just fill out one of the forms. + +>>> data = { +... 'choices-COUNT': '3', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> formset.clean_data +[{'votes': 100, 'choice': u'Calexico'}] + + +And once again, if we try to partially complete a form, validation will fail. + +>>> data = { +... 'choices-COUNT': '3', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': 'The Decemberists', +... 'choices-1-votes': '', # missing value +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{}, {'votes': [u'This field is required.']}] + + +The num_extra argument also works when the formset is pre-filled with initial +data. + +>>> initial = [{'choice': u'Calexico', 'votes': 100}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.form_list: +... print form.as_ul() +
  • Choice:
  • +
  • Votes:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Choice:
  • +
  • Votes:
  • + + +If we try to skip a form, even if it was initially displayed as blank, we will +get an error. + +>>> data = { +... 'choices-COUNT': '4', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '12', +... 'choices-3-choice': '', +... 'choices-3-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{}, {'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}] + + +# FormSets with deletion ###################################################### + +We can easily add deletion ability to a FormSet with an agrument to +formset_for_form. This will add a boolean field to each form instance. When +that boolean field is True, the cleaned data will be in formset.deleted_data +rather than formset.clean_data + +>>> ChoiceFormSet = formset_for_form(Choice, deletable=True) + +>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.form_list: +... print form.as_ul() +
  • Choice:
  • +
  • Votes:
  • +
  • Delete:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Delete:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Delete:
  • + +To delete something, we just need to set that form's special delete field to +'on'. Let's go ahead and delete Fergie. + +>>> data = { +... 'choices-COUNT': '3', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-DELETE': '', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-DELETE': 'on', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... 'choices-2-DELETE': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> formset.clean_data +[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}] +>>> formset.deleted_data +[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}] + +# FormSets with ordering ###################################################### + +We can also add ordering ability to a FormSet with an agrument to +formset_for_form. This will add a integer field to each form instance. When +form validation succeeds, formset.clean_data will have the data in the correct +order specified by the ordering fields. If a number is duplicated in the set +of ordering fields, for instance form 0 and form 3 are both marked as 1, then +the form index used as a secondary ordering criteria. In order to put +something at the front of the list, you'd need to set it's order to 0. + +>>> ChoiceFormSet = formset_for_form(Choice, orderable=True) + +>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.form_list: +... print form.as_ul() +
  • Choice:
  • +
  • Votes:
  • +
  • Order:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Order:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Order:
  • + +>>> data = { +... 'choices-COUNT': '3', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '0', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for clean_data in formset.clean_data: +... print clean_data +{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'} +{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} +{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} + +# FormSets with ordering + deletion ########################################### + +Let's try throwing ordering and deletion into the same form. + +>>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True) + +>>> initial = [ +... {'choice': u'Calexico', 'votes': 100}, +... {'choice': u'Fergie', 'votes': 900}, +... {'choice': u'The Decemberists', 'votes': 500}, +... ] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.form_list: +... print form.as_ul() +
  • Choice:
  • +
  • Votes:
  • +
  • Order:
  • +
  • Delete:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Order:
  • +
  • Delete:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Order:
  • +
  • Delete:
  • +
  • Choice:
  • +
  • Votes:
  • +
  • Order:
  • +
  • Delete:
  • + +Let's delete Fergie, and put The Decemberists ahead of Calexico. + +>>> data = { +... 'choices-COUNT': '4', # the number of forms rendered +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-0-DELETE': '', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-1-DELETE': 'on', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '0', +... 'choices-2-DELETE': '', +... 'choices-3-choice': '', +... 'choices-3-votes': '', +... 'choices-3-ORDER': '4', +... 'choices-3-DELETE': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for clean_data in formset.clean_data: +... print clean_data +{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'} +{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'} +>>> formset.deleted_data +[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}] + + +""" diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 839b8fc5d9..afe8b204c6 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from localflavor import localflavor_tests from regressions import regression_tests +from formsets import formset_tests form_tests = r""" >>> from django.newforms import * @@ -2898,158 +2899,6 @@ True >>> 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() -
  • Choice:
  • -
  • Votes:
  • - - -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() - - ->>> for form in form_set.get_forms(): # print pre-filled forms -... print form.as_ul() -
  • Choice:
  • -
  • Votes:
  • -
  • Choice:
  • -
  • Votes:
  • -
  • Choice:
  • -
  • Votes:
  • - - -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() -
  • Choice:
  • -
  • Votes:
  • -
  • Delete:
  • -
  • Choice:
  • -
  • Votes:
  • -
  • Delete:
  • -
  • Choice:
  • -
  • Votes:
  • -
  • Delete:
  • - ->>> 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() -
  • Choice:
  • -
  • Votes:
  • -
  • Order:
  • -
  • Choice:
  • -
  • Votes:
  • -
  • Order:
  • -
  • Choice:
  • -
  • Votes:
  • -
  • Order:
  • - ->>> 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) @@ -3451,6 +3300,7 @@ __test__ = { 'form_tests': form_tests, 'localflavor': localflavor_tests, 'regressions': regression_tests, + 'formset_tests': formset_tests, } if __name__ == "__main__":