mirror of https://github.com/django/django.git
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:
parent
059d9205d4
commit
7adffaeaf6
1
AUTHORS
1
AUTHORS
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in New Issue