diff -r 5ff1fc726848 -r c6bca38c1cbf parts/django/docs/topics/forms/formsets.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/parts/django/docs/topics/forms/formsets.txt Sat Jan 08 11:20:57 2011 +0530 @@ -0,0 +1,440 @@ +.. _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.forms: + ... print form.as_table() + + + +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) + +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() + + + + + + + +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 `. + +.. _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.forms: + ... print form.as_table() + + + +.. 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) + >>> 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 +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'16 June 1904', + ... '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). + +.. versionadded:: 1.1 + +``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'16 June 1904', + ... 'form-1-title': u'Test', + ... 'form-1-pub_date': u'23 June 1912', + ... } + >>> 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.forms: + ... print form.as_table() + + + + + + + + + + +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.forms: + .... print form.as_table() + + + + + + + + + + + +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.forms: + ... print form.as_table() + + + + +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 + +
+ {{ formset.management_form }} + + {% for form in formset.forms %} + {{ form }} + {% endfor %} +
+
+ +However the above can be slightly shortcutted and let the formset itself deal +with the management form: + +.. code-block:: html+django + +
+ + {{ formset }} +
+
+ +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.