mirror of
https://github.com/django/django.git
synced 2024-11-30 07:06:18 +01:00
413ceb57b2
git-svn-id: http://code.djangoproject.com/svn/django/trunk@15874 bcc190cf-cafb-0310-a4f2-bffc1f526a37
458 lines
20 KiB
Plaintext
458 lines
20 KiB
Plaintext
.. _formsets:
|
|
|
|
Formsets
|
|
========
|
|
|
|
A formset is a layer of abstraction to working with multiple forms on the same
|
|
page. It can be best compared to a data grid. Let's say you have the following
|
|
form::
|
|
|
|
>>> from django import forms
|
|
>>> class ArticleForm(forms.Form):
|
|
... title = forms.CharField()
|
|
... pub_date = forms.DateField()
|
|
|
|
You might want to allow the user to create several articles at once. To create
|
|
a formset out of an ``ArticleForm`` you would do::
|
|
|
|
>>> from django.forms.formsets import formset_factory
|
|
>>> ArticleFormSet = formset_factory(ArticleForm)
|
|
|
|
You now have created a formset named ``ArticleFormSet``. The formset gives you
|
|
the ability to iterate over the forms in the formset and display them as you
|
|
would with a regular form::
|
|
|
|
>>> formset = ArticleFormSet()
|
|
>>> for form in formset:
|
|
... 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-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
|
|
|
|
As you can see it only displayed one empty form. The number of empty forms
|
|
that is displayed is controlled by the ``extra`` parameter. By default,
|
|
``formset_factory`` defines one extra form; the following example will
|
|
display two blank forms::
|
|
|
|
>>> 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 :meth:`__iter__()` method.
|
|
|
|
Using initial data with a formset
|
|
---------------------------------
|
|
|
|
Initial data is what drives the main usability of a formset. As shown above
|
|
you can define the number of extra forms. What this means is that you are
|
|
telling the formset how many additional forms to show in addition to the
|
|
number of forms it generates from the initial data. Lets take a look at an
|
|
example::
|
|
|
|
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
|
|
>>> formset = ArticleFormSet(initial=[
|
|
... {'title': u'Django is now open source',
|
|
... 'pub_date': datetime.date.today()},
|
|
... ])
|
|
|
|
>>> for form in formset:
|
|
... 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-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-1-title">Title:</label></th><td><input type="text" name="form-1-title" id="id_form-1-title" /></td></tr>
|
|
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" id="id_form-1-pub_date" /></td></tr>
|
|
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
|
|
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
|
|
|
|
There are now a total of three forms showing above. One for the initial data
|
|
that was passed in and two extra forms. Also note that we are passing in a
|
|
list of dictionaries as the initial data.
|
|
|
|
.. seealso::
|
|
|
|
:ref:`Creating formsets from models with model formsets <model-formsets>`.
|
|
|
|
.. _formsets-max-num:
|
|
|
|
Limiting the maximum number of forms
|
|
------------------------------------
|
|
|
|
The ``max_num`` parameter to ``formset_factory`` gives you the ability to
|
|
limit the maximum number of empty forms the formset will display::
|
|
|
|
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
|
|
>>> formset = ArticleFormset()
|
|
>>> for form in formset:
|
|
... 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-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
|
|
|
|
.. versionchanged:: 1.2
|
|
|
|
If the value of ``max_num`` is greater than the number of existing
|
|
objects, up to ``extra`` additional blank forms will be added to the formset,
|
|
so long as the total number of forms does not exceed ``max_num``.
|
|
|
|
A ``max_num`` value of ``None`` (the default) puts no limit on the number of
|
|
forms displayed. Please note that the default value of ``max_num`` was changed
|
|
from ``0`` to ``None`` in version 1.2 to allow ``0`` as a valid value.
|
|
|
|
Formset validation
|
|
------------------
|
|
|
|
Validation with a formset is almost identical to a regular ``Form``. There is
|
|
an ``is_valid`` method on the formset to provide a convenient way to validate
|
|
all forms in the formset::
|
|
|
|
>>> ArticleFormSet = formset_factory(ArticleForm)
|
|
>>> data = {
|
|
... 'form-TOTAL_FORMS': u'1',
|
|
... 'form-INITIAL_FORMS': u'0',
|
|
... 'form-MAX_NUM_FORMS': u'',
|
|
... }
|
|
>>> formset = ArticleFormSet(data)
|
|
>>> formset.is_valid()
|
|
True
|
|
|
|
We passed in no data to the formset which is resulting in a valid form. The
|
|
formset is smart enough to ignore extra forms that were not changed. If we
|
|
provide an invalid article::
|
|
|
|
>>> data = {
|
|
... 'form-TOTAL_FORMS': u'2',
|
|
... 'form-INITIAL_FORMS': u'0',
|
|
... 'form-MAX_NUM_FORMS': u'',
|
|
... 'form-0-title': u'Test',
|
|
... 'form-0-pub_date': u'1904-06-16',
|
|
... 'form-1-title': u'Test',
|
|
... 'form-1-pub_date': u'', # <-- this date is missing but required
|
|
... }
|
|
>>> formset = ArticleFormSet(data)
|
|
>>> formset.is_valid()
|
|
False
|
|
>>> formset.errors
|
|
[{}, {'pub_date': [u'This field is required.']}]
|
|
|
|
As we can see, ``formset.errors`` is a list whose entries correspond to the
|
|
forms in the formset. Validation was performed for each of the two forms, and
|
|
the expected error message appears for the second item.
|
|
|
|
.. _understanding-the-managementform:
|
|
|
|
Understanding the ManagementForm
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
You may have noticed the additional data (``form-TOTAL_FORMS``,
|
|
``form-INITIAL_FORMS`` and ``form-MAX_NUM_FORMS``) that was required
|
|
in the formset's data above. This data is required for the
|
|
``ManagementForm``. This form is used by the formset to manage the
|
|
collection of forms contained in the formset. If you don't provide
|
|
this management data, an exception will be raised::
|
|
|
|
>>> data = {
|
|
... 'form-0-title': u'Test',
|
|
... 'form-0-pub_date': u'',
|
|
... }
|
|
>>> formset = ArticleFormSet(data)
|
|
Traceback (most recent call last):
|
|
...
|
|
django.forms.util.ValidationError: [u'ManagementForm data is missing or has been tampered with']
|
|
|
|
It is used to keep track of how many form instances are being displayed. If
|
|
you are adding new forms via JavaScript, you should increment the count fields
|
|
in this form as well.
|
|
|
|
The management form is available as an attribute of the formset
|
|
itself. When rendering a formset in a template, you can include all
|
|
the management data by rendering ``{{ my_formset.management_form }}``
|
|
(substituting the name of your formset as appropriate).
|
|
|
|
``total_form_count`` and ``initial_form_count``
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
``BaseFormSet`` has a couple of methods that are closely related to the
|
|
``ManagementForm``, ``total_form_count`` and ``initial_form_count``.
|
|
|
|
``total_form_count`` returns the total number of forms in this formset.
|
|
``initial_form_count`` returns the number of forms in the formset that were
|
|
pre-filled, and is also used to determine how many forms are required. You
|
|
will probably never need to override either of these methods, so please be
|
|
sure you understand what they do before doing so.
|
|
|
|
.. versionadded:: 1.2
|
|
|
|
``empty_form``
|
|
~~~~~~~~~~~~~~
|
|
|
|
``BaseFormSet`` provides an additional attribute ``empty_form`` which returns
|
|
a form instance with a prefix of ``__prefix__`` for easier use in dynamic
|
|
forms with JavaScript.
|
|
|
|
Custom formset validation
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
A formset has a ``clean`` method similar to the one on a ``Form`` class. This
|
|
is where you define your own validation that works at the formset level::
|
|
|
|
>>> from django.forms.formsets import BaseFormSet
|
|
|
|
>>> class BaseArticleFormSet(BaseFormSet):
|
|
... def clean(self):
|
|
... """Checks that no two articles have the same title."""
|
|
... if any(self.errors):
|
|
... # Don't bother validating the formset unless each form is valid on its own
|
|
... return
|
|
... titles = []
|
|
... for i in range(0, self.total_form_count()):
|
|
... form = self.forms[i]
|
|
... title = form.cleaned_data['title']
|
|
... if title in titles:
|
|
... raise forms.ValidationError("Articles in a set must have distinct titles.")
|
|
... titles.append(title)
|
|
|
|
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
|
|
>>> data = {
|
|
... 'form-TOTAL_FORMS': u'2',
|
|
... 'form-INITIAL_FORMS': u'0',
|
|
... 'form-MAX_NUM_FORMS': u'',
|
|
... 'form-0-title': u'Test',
|
|
... 'form-0-pub_date': u'1904-06-16',
|
|
... 'form-1-title': u'Test',
|
|
... 'form-1-pub_date': u'1912-06-23',
|
|
... }
|
|
>>> formset = ArticleFormSet(data)
|
|
>>> formset.is_valid()
|
|
False
|
|
>>> formset.errors
|
|
[{}, {}]
|
|
>>> formset.non_form_errors()
|
|
[u'Articles in a set must have distinct titles.']
|
|
|
|
The formset ``clean`` method is called after all the ``Form.clean`` methods
|
|
have been called. The errors will be found using the ``non_form_errors()``
|
|
method on the formset.
|
|
|
|
Dealing with ordering and deletion of forms
|
|
-------------------------------------------
|
|
|
|
Common use cases with a formset is dealing with ordering and deletion of the
|
|
form instances. This has been dealt with for you. The ``formset_factory``
|
|
provides two optional parameters ``can_order`` and ``can_delete`` that will do
|
|
the extra work of adding the extra fields and providing simpler ways of
|
|
getting to that data.
|
|
|
|
``can_order``
|
|
~~~~~~~~~~~~~
|
|
|
|
Default: ``False``
|
|
|
|
Lets create a formset with the ability to order::
|
|
|
|
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
|
|
>>> formset = ArticleFormSet(initial=[
|
|
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
|
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
|
... ])
|
|
>>> for form in formset:
|
|
... 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-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-ORDER">Order:</label></th><td><input type="text" name="form-0-ORDER" value="1" id="id_form-0-ORDER" /></td></tr>
|
|
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
|
|
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
|
|
<tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="text" name="form-1-ORDER" value="2" id="id_form-1-ORDER" /></td></tr>
|
|
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
|
|
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
|
|
<tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="text" name="form-2-ORDER" id="id_form-2-ORDER" /></td></tr>
|
|
|
|
This adds an additional field to each form. This new field is named ``ORDER``
|
|
and is an ``forms.IntegerField``. For the forms that came from the initial
|
|
data it automatically assigned them a numeric value. Lets look at what will
|
|
happen when the user changes these values::
|
|
|
|
>>> data = {
|
|
... 'form-TOTAL_FORMS': u'3',
|
|
... 'form-INITIAL_FORMS': u'2',
|
|
... 'form-MAX_NUM_FORMS': u'',
|
|
... 'form-0-title': u'Article #1',
|
|
... 'form-0-pub_date': u'2008-05-10',
|
|
... 'form-0-ORDER': u'2',
|
|
... 'form-1-title': u'Article #2',
|
|
... 'form-1-pub_date': u'2008-05-11',
|
|
... 'form-1-ORDER': u'1',
|
|
... 'form-2-title': u'Article #3',
|
|
... 'form-2-pub_date': u'2008-05-01',
|
|
... 'form-2-ORDER': u'0',
|
|
... }
|
|
|
|
>>> formset = ArticleFormSet(data, initial=[
|
|
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
|
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
|
... ])
|
|
>>> formset.is_valid()
|
|
True
|
|
>>> for form in formset.ordered_forms:
|
|
... print form.cleaned_data
|
|
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': u'Article #3'}
|
|
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': u'Article #2'}
|
|
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': u'Article #1'}
|
|
|
|
``can_delete``
|
|
~~~~~~~~~~~~~~
|
|
|
|
Default: ``False``
|
|
|
|
Lets create a formset with the ability to delete::
|
|
|
|
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
|
|
>>> formset = ArticleFormSet(initial=[
|
|
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
|
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
|
... ])
|
|
>>> for form in formset:
|
|
.... 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" />
|
|
<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-DELETE">Delete:</label></th><td><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /></td></tr>
|
|
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
|
|
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
|
|
<tr><th><label for="id_form-1-DELETE">Delete:</label></th><td><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /></td></tr>
|
|
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
|
|
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
|
|
<tr><th><label for="id_form-2-DELETE">Delete:</label></th><td><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /></td></tr>
|
|
|
|
Similar to ``can_order`` this adds a new field to each form named ``DELETE``
|
|
and is a ``forms.BooleanField``. When data comes through marking any of the
|
|
delete fields you can access them with ``deleted_forms``::
|
|
|
|
>>> data = {
|
|
... 'form-TOTAL_FORMS': u'3',
|
|
... 'form-INITIAL_FORMS': u'2',
|
|
... 'form-MAX_NUM_FORMS': u'',
|
|
... 'form-0-title': u'Article #1',
|
|
... 'form-0-pub_date': u'2008-05-10',
|
|
... 'form-0-DELETE': u'on',
|
|
... 'form-1-title': u'Article #2',
|
|
... 'form-1-pub_date': u'2008-05-11',
|
|
... 'form-1-DELETE': u'',
|
|
... 'form-2-title': u'',
|
|
... 'form-2-pub_date': u'',
|
|
... 'form-2-DELETE': u'',
|
|
... }
|
|
|
|
>>> formset = ArticleFormSet(data, initial=[
|
|
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
|
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
|
... ])
|
|
>>> [form.cleaned_data for form in formset.deleted_forms]
|
|
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': u'Article #1'}]
|
|
|
|
Adding additional fields to a formset
|
|
-------------------------------------
|
|
|
|
If you need to add additional fields to the formset this can be easily
|
|
accomplished. The formset base class provides an ``add_fields`` method. You
|
|
can simply override this method to add your own fields or even redefine the
|
|
default fields/attributes of the order and deletion fields::
|
|
|
|
>>> class BaseArticleFormSet(BaseFormSet):
|
|
... def add_fields(self, form, index):
|
|
... super(BaseArticleFormSet, self).add_fields(form, index)
|
|
... form.fields["my_field"] = forms.CharField()
|
|
|
|
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
|
|
>>> formset = ArticleFormSet()
|
|
>>> for form in formset:
|
|
... 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-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-my_field">My field:</label></th><td><input type="text" name="form-0-my_field" id="id_form-0-my_field" /></td></tr>
|
|
|
|
Using a formset in views and templates
|
|
--------------------------------------
|
|
|
|
Using a formset inside a view is as easy as using a regular ``Form`` class.
|
|
The only thing you will want to be aware of is making sure to use the
|
|
management form inside the template. Let's look at a sample view:
|
|
|
|
.. code-block:: python
|
|
|
|
def manage_articles(request):
|
|
ArticleFormSet = formset_factory(ArticleForm)
|
|
if request.method == 'POST':
|
|
formset = ArticleFormSet(request.POST, request.FILES)
|
|
if formset.is_valid():
|
|
# do something with the formset.cleaned_data
|
|
pass
|
|
else:
|
|
formset = ArticleFormSet()
|
|
return render_to_response('manage_articles.html', {'formset': formset})
|
|
|
|
The ``manage_articles.html`` template might look like this:
|
|
|
|
.. code-block:: html+django
|
|
|
|
<form method="post" action="">
|
|
{{ formset.management_form }}
|
|
<table>
|
|
{% for form in formset %}
|
|
{{ form }}
|
|
{% endfor %}
|
|
</table>
|
|
</form>
|
|
|
|
However the above can be slightly shortcutted and let the formset itself deal
|
|
with the management form:
|
|
|
|
.. code-block:: html+django
|
|
|
|
<form method="post" action="">
|
|
<table>
|
|
{{ formset }}
|
|
</table>
|
|
</form>
|
|
|
|
The above ends up calling the ``as_table`` method on the formset class.
|
|
|
|
Using more than one formset in a view
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
You are able to use more than one formset in a view if you like. Formsets
|
|
borrow much of its behavior from forms. With that said you are able to use
|
|
``prefix`` to prefix formset form field names with a given value to allow
|
|
more than one formset to be sent to a view without name clashing. Lets take
|
|
a look at how this might be accomplished:
|
|
|
|
.. code-block:: python
|
|
|
|
def manage_articles(request):
|
|
ArticleFormSet = formset_factory(ArticleForm)
|
|
BookFormSet = formset_factory(BookForm)
|
|
if request.method == 'POST':
|
|
article_formset = ArticleFormSet(request.POST, request.FILES, prefix='articles')
|
|
book_formset = BookFormSet(request.POST, request.FILES, prefix='books')
|
|
if article_formset.is_valid() and book_formset.is_valid():
|
|
# do something with the cleaned_data on the formsets.
|
|
pass
|
|
else:
|
|
article_formset = ArticleFormSet(prefix='articles')
|
|
book_formset = BookFormSet(prefix='books')
|
|
return render_to_response('manage_articles.html', {
|
|
'article_formset': article_formset,
|
|
'book_formset': book_formset,
|
|
})
|
|
|
|
You would then render the formsets as normal. It is important to point out
|
|
that you need to pass ``prefix`` on both the POST and non-POST cases so that
|
|
it is rendered and processed correctly.
|