1
0
mirror of https://github.com/django/django.git synced 2025-07-04 01:39:20 +00:00

newforms-admin: Rewrote most of FormSets and associated tests. They now handle an arbitrary number of extra forms, and deletion and ordering at the same time. BaseFormSet still needs some cleanup, but the tests pass.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@4953 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Joseph Kocherhans 2007-04-07 05:56:42 +00:00
parent c1f84583de
commit 203f93a7aa
3 changed files with 525 additions and 261 deletions

View File

@ -5,6 +5,11 @@ FORM_COUNT_FIELD_NAME = 'COUNT'
ORDERING_FIELD_NAME = 'ORDER' ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE' 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): class ManagementForm(forms.Form):
""" """
``ManagementForm`` is used to keep track of how many form instances ``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) self.base_fields[FORM_COUNT_FIELD_NAME] = forms.IntegerField(widget=forms.HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs) super(ManagementForm, self).__init__(*args, **kwargs)
class FormSet(object): class BaseFormSet(object):
"""A collection of instances of the same Form class.""" """A collection of instances of the same Form class."""
def __init__(self, form_class, data=None, auto_id='id_%s', prefix=None, initial=None): def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
self.form_class = form_class self.is_bound = data is not None
self.prefix = prefix or 'form' self.prefix = prefix or 'form'
self.auto_id = auto_id self.auto_id = auto_id
self.data = data
self.initial = initial
# initialization is different depending on whether we recieved data, initial, or nothing # initialization is different depending on whether we recieved data, initial, or nothing
if data: if data:
self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix) self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
if self.management_form.is_valid(): 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: else:
# not sure that ValidationError is the best thing to raise here # not sure that ValidationError is the best thing to raise here
raise forms.ValidationError('ManagementForm data is missing or has been tampered with') 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: elif initial:
form_count = len(initial) self.required_forms = len(initial)
self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: form_count+1}, auto_id=self.auto_id, prefix=self.prefix) self.total_forms = self.required_forms + self.num_extra
self.form_list = self._forms_for_initial(initial, form_count=form_count) self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
else: else:
self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: 1}, auto_id=self.auto_id, prefix=self.prefix) self.required_forms = 0
self.form_list = self._empty_forms(form_count=1) 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 def _get_form_list(self):
# TODO: allow more than 1 extra blank form to be displayed """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 = property(_get_form_list)
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): def full_clean(self):
form_list = [] """
# generate a form for each item in initial, plus one empty one Cleans all of self.data and populates self.__errors and self.clean_data.
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)) is_valid = True
self.add_fields(form_instance, i)
form_list.append(form_instance) errors = []
# add 1 empty form if not self.is_bound: # Stop further processing.
form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(i+1)) self.__errors = errors
self.add_fields(form_instance, i+1) return
form_list.append(form_instance) clean_data = []
return form_list 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): deleted_data.reverse()
form_list = [] if self.orderable:
# we only need one form, there's no inital data and no post data clean_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
form_instance = self.form_class(auto_id=self.auto_id, prefix=self.add_prefix(0)) else:
form_list.append(form_instance) clean_data.reverse()
return form_list errors.reverse()
self._form_list.reverse()
def get_forms(self):
return self.form_list 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): def add_fields(self, form, index):
"""A hook for adding extra fields on to each form instance.""" """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): def add_prefix(self, index):
return '%s-%s' % (self.prefix, 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): def is_valid(self):
for form in self.get_non_empty_forms(): self.full_clean()
if not form.is_valid(): return self._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: handle deletion and ordering in the same FormSet
# TODO: model integration: form_for_instance and form_for_model type functions # TODO: model integration: form_for_instance and form_for_model type functions

View File

@ -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()
<li>Choice: <input type="text" name="choices-0-choice" /></li>
<li>Votes: <input type="text" name="choices-0-votes" /></li>
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()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="text" name="choices-1-votes" /></li>
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()
<li>Choice: <input type="text" name="choices-0-choice" /></li>
<li>Votes: <input type="text" name="choices-0-votes" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="text" name="choices-1-votes" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>
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()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="text" name="choices-1-votes" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
<li>Votes: <input type="text" name="choices-3-votes" /></li>
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()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
<li>Votes: <input type="text" name="choices-1-votes" value="900" /></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>
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()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
<li>Votes: <input type="text" name="choices-1-votes" value="900" /></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': '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()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li>
<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li>
<li>Votes: <input type="text" name="choices-2-votes" value="500" /></li>
<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>
<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
<li>Votes: <input type="text" name="choices-3-votes" /></li>
<li>Order: <input type="text" name="choices-3-ORDER" value="4" /></li>
<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>
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'}]
"""

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from localflavor import localflavor_tests from localflavor import localflavor_tests
from regressions import regression_tests from regressions import regression_tests
from formsets import formset_tests
form_tests = r""" form_tests = r"""
>>> from django.newforms import * >>> from django.newforms import *
@ -2898,158 +2899,6 @@ True
>>> p.clean_data >>> p.clean_data
{'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)} {'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 ################################################ # Forms with NullBooleanFields ################################################
NullBooleanField is a bit of a special case because its presentation (widget) NullBooleanField is a bit of a special case because its presentation (widget)
@ -3451,6 +3300,7 @@ __test__ = {
'form_tests': form_tests, 'form_tests': form_tests,
'localflavor': localflavor_tests, 'localflavor': localflavor_tests,
'regressions': regression_tests, 'regressions': regression_tests,
'formset_tests': formset_tests,
} }
if __name__ == "__main__": if __name__ == "__main__":