1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

newforms-admin: Added documentation on formsets.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7606 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Brian Rosner 2008-06-10 04:40:13 +00:00
parent 1fd3db4ab0
commit c6b53bec10
2 changed files with 403 additions and 0 deletions

View File

@ -369,3 +369,95 @@ There are a couple of things to note, however.
Chances are these notes won't affect you unless you're trying to do something Chances are these notes won't affect you unless you're trying to do something
tricky with subclassing. tricky with subclassing.
Model Formsets
==============
Similar to regular formsets there are a couple enhanced formset classes that
provide all the right things to work with your models. Lets reuse the
``Author`` model from above::
>>> from django.newforms.models import modelformset_factory
>>> AuthorFormSet = modelformset_factory(Author)
This will create a formset that is capable of working with the data associated
to the ``Author`` model. It works just like a regular formset::
>>> formset = AuthorFormSet()
>>> print formset
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" />
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></td></tr>
<tr><th><label for="id_form-0-title">Title:</label></th><td><select name="form-0-title" id="id_form-0-title">
<option value="" selected="selected">---------</option>
<option value="MR">Mr.</option>
<option value="MRS">Mrs.</option>
<option value="MS">Ms.</option>
</select></td></tr>
<tr><th><label for="id_form-0-birth_date">Birth date:</label></th><td><input type="text" name="form-0-birth_date" id="id_form-0-birth_date" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></td></tr>
.. note::
One thing to note is that ``modelformset_factory`` uses ``formset_factory``
and by default uses ``can_delete=True``.
Changing the queryset
~~~~~~~~~~~~~~~~~~~~~
By default when you create a formset from a model the queryset will be all
objects in the model. This is best shown as ``Author.objects.all()``. This is
configurable::
>>> formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))
Alternatively, you can use a subclassing based approach::
from django.newforms.models import BaseModelFormSet
class BaseAuthorFormSet(BaseModelFormSet):
def get_queryset(self):
return super(BaseAuthorFormSet, self).get_queryset().filter(name__startswith='O')
Then your ``BaseAuthorFormSet`` would be passed into the factory function to
be used as a base::
>>> AuthorFormSet = modelformset_factory(Author, formset=BaseAuthorFormSet)
Saving objects in the formset
-----------------------------
Similar to a ``ModelForm`` you can save the data into the model. This is done
with the ``save()`` method on the formset::
# create a formset instance with POST data.
>>> formset = AuthorFormSet(request.POST)
# assuming all is valid, save the data
>>> instances = formset.save()
The ``save()`` method will return the instances that have been saved to the
database. If an instance did not change in the bound data it will not be
saved to the database and not found in ``instances`` in the above example.
You can optionally pass in ``commit=False`` to ``save()`` to only return the
model instances without any database interaction::
# don't save to the database
>>> instances = formset.save(commit=False)
>>> for instance in instances:
... # do something with instance
... instance.save()
This gives you the ability to attach data to the instances before saving them
to the database.
Using ``inlineformset_factory``
-------------------------------
The ``inlineformset_factory`` is a helper to a common usage pattern of working
with related objects through a foreign key. Suppose you have two models
``Author`` and ``Book``. You want to create a formset that works with the
books of a specific author. Here is how you could accomplish this::
>>> from django.newforms.models import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book)
>>> author = Author.objects.get(name=u'Orson Scott Card')
>>> formset = BookFormSet(instance=author)

View File

@ -2168,6 +2168,317 @@ layout -- simply add a media declaration to the form::
<script type="text/javascript" src="http://media.example.com/actions.js"></script> <script type="text/javascript" src="http://media.example.com/actions.js"></script>
<script type="text/javascript" src="http://media.example.com/whizbang.js"></script> <script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
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 newforms as 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 of ``ArticleForm``s you would do::
>>> from django.newforms.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.forms:
... 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 form. This is because by default the
``formset_factory`` defines one extra form. This can be controlled with the
``extra`` parameter::
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
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.forms:
... 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.
Formset validation
------------------
Validation with a formset is about identical to a regular ``Form``. There is
an ``is_valid`` method on the formset to provide a convenient way to validate
each form in the formset::
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> formset = ArticleFormSet({})
>>> 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
attempt to provide an article, but fail to do so::
>>> data = {
... 'form-TOTAL_FORMS': u'1',
... 'form-INITIAL_FORMS': u'1',
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{'pub_date': [u'This field is required.']}]
As we can see the formset properly performed validation and gave us the
expected errors.
Understanding the ManagementForm
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You may have noticed the additional data that was required in the formset's
data above. This data is coming from the ``ManagementForm``. This form is
dealt with internally to the formset. If you don't use it, it will result in
an exception::
>>> data = {
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
Traceback (most recent call last):
...
django.newforms.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.
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 deals at the formset level::
>>> from django.newforms.formsets import BaseFormSet
>>> class BaseArticleFormSet(BaseFormSet):
... def clean(self):
... raise forms.ValidationError, u'An error occured.'
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet({})
>>> formset.is_valid()
False
>>> formset.non_form_errors()
[u'An error occured.']
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.forms:
... 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-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.forms:
.... 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" />
<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-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.forms:
... 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 formsets 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. Lets look at a sample view::
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
else:
formset = ArticleFormSet()
return render_to_response('manage_articles.html', {'formset': formset})
The ``manage_articles.html`` template might look like this::
<form method="POST" action="">
{{ formset.management_form }}
<table>
{% for form in formset.forms %}
{{ form }}
{% endfor %}
</table>
</form>
However the above can be slightly shortcutted and let the formset itself deal
with the management form::
<form method="POST" action="">
<table>
{{ formset }}
</table>
</form>
The above ends up calling the ``as_table`` method on the formset class.
More coming soon More coming soon
================ ================