Fixed #14655 -- Made formsets iterable. This allows a slightly more natural iteration API (`for form in formsets`), and allows you to easily override the form rendering order. Thanks to Kent Hauser for the suggestion and patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14986 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-12-19 13:41:43 +00:00
parent 059d9205d4
commit 7adffaeaf6
5 changed files with 74 additions and 16 deletions

View File

@ -216,6 +216,7 @@ answer newbie questions, and generally made Django that much better:
Brant Harris Brant Harris
Ronny Haryanto <http://ronny.haryan.to/> Ronny Haryanto <http://ronny.haryan.to/>
Hawkeye Hawkeye
Kent Hauser <kent@khauser.net>
Joe Heck <http://www.rhonabwy.com/wp/> Joe Heck <http://www.rhonabwy.com/wp/>
Joel Heenan <joelh-django@planetjoel.com> Joel Heenan <joelh-django@planetjoel.com>
Mikko Hellsing <mikko@sorl.net> Mikko Hellsing <mikko@sorl.net>

View File

@ -49,6 +49,17 @@ class BaseFormSet(StrAndUnicode):
def __unicode__(self): def __unicode__(self):
return self.as_table() return self.as_table()
def __iter__(self):
"""Yields the forms in the order they should be rendered"""
return iter(self.forms)
def __getitem__(self, index):
"""Returns the form at the given index, based on the rendering order"""
return list(self)[index]
def __len__(self):
return len(self.forms)
def _management_form(self): def _management_form(self):
"""Returns the ManagementForm instance for this FormSet.""" """Returns the ManagementForm instance for this FormSet."""
if self.is_bound: if self.is_bound:
@ -323,17 +334,17 @@ class BaseFormSet(StrAndUnicode):
# XXX: there is no semantic division between forms here, there # XXX: there is no semantic division between forms here, there
# probably should be. It might make sense to render each form as a # probably should be. It might make sense to render each form as a
# table row with each field as a td. # table row with each field as a td.
forms = u' '.join([form.as_table() for form in self.forms]) forms = u' '.join([form.as_table() for form in self])
return mark_safe(u'\n'.join([unicode(self.management_form), forms])) return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
def as_p(self): def as_p(self):
"Returns this formset rendered as HTML <p>s." "Returns this formset rendered as HTML <p>s."
forms = u' '.join([form.as_p() for form in self.forms]) forms = u' '.join([form.as_p() for form in self])
return mark_safe(u'\n'.join([unicode(self.management_form), forms])) return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
def as_ul(self): def as_ul(self):
"Returns this formset rendered as HTML <li>s." "Returns this formset rendered as HTML <li>s."
forms = u' '.join([form.as_ul() for form in self.forms]) forms = u' '.join([form.as_ul() for form in self])
return mark_safe(u'\n'.join([unicode(self.management_form), forms])) return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,

View File

@ -23,7 +23,7 @@ the ability to iterate over the forms in the formset and display them as you
would with a regular form:: would with a regular form::
>>> formset = ArticleFormSet() >>> formset = ArticleFormSet()
>>> for form in formset.forms: >>> for form in formset:
... print form.as_table() ... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr> <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr> <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
@ -35,6 +35,20 @@ display two blank forms::
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2) >>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
.. versionchanged:: 1.3
Prior to Django 1.3, formset instances were not iterable. To render
the formset you iterated over the ``forms`` attribute::
>>> formset = ArticleFormSet()
>>> for form in formset.forms:
... print form.as_table()
Iterating over ``formset.forms`` will render the forms in the order
they were created. The default formset iterator also renders the forms
in this order, but you can change this order by providing an alternate
implementation for the :method:`__iter__()` method.
Using initial data with a formset Using initial data with a formset
--------------------------------- ---------------------------------
@ -50,7 +64,7 @@ example::
... 'pub_date': datetime.date.today()}, ... 'pub_date': datetime.date.today()},
... ]) ... ])
>>> for form in formset.forms: >>> for form in formset:
... print form.as_table() ... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title" /></td></tr> <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-12" id="id_form-0-pub_date" /></td></tr> <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-12" id="id_form-0-pub_date" /></td></tr>
@ -77,7 +91,7 @@ limit the maximum number of empty forms the formset will display::
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1) >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
>>> formset = ArticleFormset() >>> formset = ArticleFormset()
>>> for form in formset.forms: >>> for form in formset:
... print form.as_table() ... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr> <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr> <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
@ -250,7 +264,7 @@ Lets create a formset with the ability to order::
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)}, ... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ]) ... ])
>>> for form in formset.forms: >>> for form in formset:
... print form.as_table() ... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr> <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr> <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
@ -306,7 +320,7 @@ Lets create a formset with the ability to delete::
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)}, ... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ]) ... ])
>>> for form in formset.forms: >>> for form in formset:
.... print form.as_table() .... print form.as_table()
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" /> <input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr> <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
@ -360,7 +374,7 @@ default fields/attributes of the order and deletion fields::
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet) >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet() >>> formset = ArticleFormSet()
>>> for form in formset.forms: >>> for form in formset:
... print form.as_table() ... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr> <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr> <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
@ -393,7 +407,7 @@ The ``manage_articles.html`` template might look like this:
<form method="post" action=""> <form method="post" action="">
{{ formset.management_form }} {{ formset.management_form }}
<table> <table>
{% for form in formset.forms %} {% for form in formset %}
{{ form }} {{ form }}
{% endfor %} {% endfor %}
</table> </table>

View File

@ -684,7 +684,7 @@ so long as the total number of forms does not exceed ``max_num``::
>>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=2) >>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=2)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name')) >>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> for form in formset.forms: >>> for form in formset:
... print form.as_table() ... print form.as_table()
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-0-id" value="1" id="id_form-0-id" /></td></tr> <tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-0-id" value="1" id="id_form-0-id" /></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><input id="id_form-1-name" type="text" name="form-1-name" value="Paul Verlaine" maxlength="100" /><input type="hidden" name="form-1-id" value="3" id="id_form-1-id" /></td></tr> <tr><th><label for="id_form-1-name">Name:</label></th><td><input id="id_form-1-name" type="text" name="form-1-name" value="Paul Verlaine" maxlength="100" /><input type="hidden" name="form-1-id" value="3" id="id_form-1-id" /></td></tr>
@ -778,7 +778,7 @@ itself::
<form method="post" action=""> <form method="post" action="">
{{ formset.management_form }} {{ formset.management_form }}
{% for form in formset.forms %} {% for form in formset %}
{{ form }} {{ form }}
{% endfor %} {% endfor %}
</form> </form>
@ -791,7 +791,7 @@ Third, you can manually render each field::
<form method="post" action=""> <form method="post" action="">
{{ formset.management_form }} {{ formset.management_form }}
{% for form in formset.forms %} {% for form in formset %}
{% for field in form %} {% for field in form %}
{{ field.label_tag }}: {{ field }} {{ field.label_tag }}: {{ field }}
{% endfor %} {% endfor %}
@ -804,7 +804,7 @@ if you were rendering the ``name`` and ``age`` fields of a model::
<form method="post" action=""> <form method="post" action="">
{{ formset.management_form }} {{ formset.management_form }}
{% for form in formset.forms %} {% for form in formset %}
{{ form.id }} {{ form.id }}
<ul> <ul>
<li>{{ form.name }}</li> <li>{{ form.name }}</li>

View File

@ -767,6 +767,38 @@ class FormsFormsetTestCase(TestCase):
self.assertFalse(formset.is_valid()) self.assertFalse(formset.is_valid())
self.assertEqual(formset.non_form_errors(), [u'You may only specify a drink once.']) self.assertEqual(formset.non_form_errors(), [u'You may only specify a drink once.'])
def test_formset_iteration(self):
# Regression tests for #16455 -- formset instances are iterable
ChoiceFormset = formset_factory(Choice, extra=3)
formset = ChoiceFormset()
# confirm iterated formset yields formset.forms
forms = list(formset)
self.assertEqual(forms, formset.forms)
self.assertEqual(len(formset), len(forms))
# confirm indexing of formset
self.assertEqual(formset[0], forms[0])
try:
formset[3]
self.fail('Requesting an invalid formset index should raise an exception')
except IndexError:
pass
# Formets can override the default iteration order
class BaseReverseFormSet(BaseFormSet):
def __iter__(self):
for form in reversed(self.forms):
yield form
ReverseChoiceFormset = formset_factory(Choice, BaseReverseFormSet, extra=3)
reverse_formset = ReverseChoiceFormset()
# confirm that __iter__ modifies rendering order
# compare forms from "reverse" formset with forms from original formset
self.assertEqual(str(reverse_formset[0]), str(forms[-1]))
self.assertEqual(str(reverse_formset[1]), str(forms[-2]))
self.assertEqual(len(reverse_formset), len(forms))
data = { data = {
'choices-TOTAL_FORMS': '1', # the number of forms rendered 'choices-TOTAL_FORMS': '1', # the number of forms rendered
@ -802,7 +834,7 @@ class FormsetAsFooTests(TestCase):
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>""") <li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>""")
# Regression test for #11418 ################################################# # Regression test for #11418 #################################################
class ArticleForm(Form): class ArticleForm(Form):
title = CharField() title = CharField()
pub_date = DateField() pub_date = DateField()
@ -835,7 +867,7 @@ class TestIsBoundBehavior(TestCase):
'form-0-title': u'Test', 'form-0-title': u'Test',
'form-0-pub_date': u'1904-06-16', 'form-0-pub_date': u'1904-06-16',
'form-1-title': u'Test', 'form-1-title': u'Test',
'form-1-pub_date': u'', # <-- this date is missing but required 'form-1-pub_date': u'', # <-- this date is missing but required
} }
formset = ArticleFormSet(data) formset = ArticleFormSet(data)
self.assertFalse(formset.is_valid()) self.assertFalse(formset.is_valid())