All notes
Forms

Formsets

A formset is a layer of abstraction to work with multiple forms on the same page. 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 import formset_factory
ArticleFormSet = formset_factory(ArticleForm)

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)

Iterating over the formset will render the forms in the order they were created. You can change this order by providing an alternate implementation for the __iter__() method.

Formsets can also be indexed into, which returns the corresponding form. If you override __iter__, you will need to also override __getitem__ to have matching behavior.

Initial data

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.

See example below: there are a total of three forms, one for the initial data that was passed in and two extra forms.


import datetime
from django.forms import formset_factory
from myapp.forms import ArticleForm
ArticleFormSet = formset_factory(ArticleForm, extra=2)
formset = ArticleFormSet(initial=[
    {'title': '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>

Form number

The max_num parameter to formset_factory() gives you the ability to limit the number of forms the formset will display.

If the number of items in the initial data exceeds max_num, all initial data forms will be displayed regardless of the value of max_num and no extra forms will be displayed.

For example, if extra=3 and max_num=1 and the formset is initialized with two initial items, two forms with the initial data will be displayed.

A max_num value of None (the default) puts a high limit on the number of forms displayed (1000). In practice this is equivalent to no limit.

total_form_count and initial_form_count

BaseFormSet has a couple of methods that are closely related to the ManagementForm (see below), 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.

Validating the number of forms in a formset

If validate_max=True is passed to formset_factory(), validation will also check that the number of forms in the data set, minus those marked for deletion, is less than or equal to max_num.


from django.forms import formset_factory
from myapp.forms import ArticleForm
ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True)

data = {
    'form-TOTAL_FORMS': '2',
    'form-INITIAL_FORMS': '0',
    'form-MIN_NUM_FORMS': '',
    'form-MAX_NUM_FORMS': '',
    'form-0-title': 'Test',
    'form-0-pub_date': '1904-06-16',
    'form-1-title': 'Test 2',
    'form-1-pub_date': '1912-06-23',
}
formset = ArticleFormSet(data)
formset.is_valid()
# False
formset.errors
# [{}, {}]
formset.non_form_errors()
# ['Please submit 1 or fewer forms.']

# Similarly, "validate_min" and "min_num":
ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)

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.

Validation was performed for each of the two forms, and the expected error message appears for the second item:


data = {
    'form-TOTAL_FORMS': '2',
    'form-INITIAL_FORMS': '0',
    'form-MAX_NUM_FORMS': '',
    'form-0-title': 'Test',
    'form-0-pub_date': '1904-06-16',
    'form-1-title': 'Test',
    'form-1-pub_date': '', # -- this date is missing but required
}
formset = ArticleFormSet(data)
formset.is_valid()
# False
formset.errors
# [{}, {'pub_date': ['This field is required.']}]

## The errors contains an empty dict.
len(formset.errors)
# 2
## Use this func to obtain real count:
formset.total_error_count()
# 1

# Check if form data differs from the initial data (i.e. the form was sent without any data)
formset.has_changed()
# False

Custom 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 import BaseFormSet
from django.forms import formset_factory
from myapp.forms import ArticleForm

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 form in self.forms:
            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': '2',
    'form-INITIAL_FORMS': '0',
    'form-MAX_NUM_FORMS': '',
    'form-0-title': 'Test',
    'form-0-pub_date': '1904-06-16',
    'form-1-title': 'Test',
    'form-1-pub_date': '1912-06-23',
}
formset = ArticleFormSet(data)
formset.is_valid()
# False
formset.errors
# [{}, {}]
formset.non_form_errors()
# ['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.

The ManagementForm

The additional data (form-TOTAL_FORMS, form-INITIAL_FORMS and form-MAX_NUM_FORMS) was required in the formset’s data above. This data is required for the ManagementForm, which 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': 'Test',
    'form-0-pub_date': '',
}
formset = ArticleFormSet(data)
formset.is_valid()
# Traceback (most recent call last):
# ...
# django.forms.utils.ValidationError: ['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. On the other hand, if you are using JavaScript to allow deletion of existing objects, then you need to ensure the ones being removed are properly marked for deletion by including form-#-DELETE in the POST data. It is expected that all forms are present in the POST data regardless.

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 }}.

Order and Deletion

can_order 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.


rom django.forms import formset_factory
from myapp.forms import ArticleForm

ArticleFormSet = formset_factory(ArticleForm, can_order=True)

formset = ArticleFormSet(initial=[
    {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
    {'title': '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-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>

can_delete 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': '3',
    'form-INITIAL_FORMS': '2',
    'form-MAX_NUM_FORMS': '',
    'form-0-title': 'Article #1',
    'form-0-pub_date': '2008-05-10',
    'form-0-DELETE': 'on',
    'form-1-title': 'Article #2',
    'form-1-pub_date': '2008-05-11',
    'form-1-DELETE': '',
    'form-2-title': '',
    'form-2-pub_date': '',
    'form-2-DELETE': '',
}

formset = ArticleFormSet(data, initial=[
    {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
    {'title': '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': 'Article #1'}]

Manually rendered can_delete and can_order

If you manually render fields in the template, you can render can_delete parameter with {{ form.DELETE }}:


<form method="post" action="">
    {{ formset.management_form }}
    {% for form in formset %}
        <ul>
            <li>{{ form.title }}</li>
            <li>{{ form.pub_date }}</li>
            {% if formset.can_delete %}
                <li>{{ form.DELETE }}</li>
            {% endif %}
        </ul>
    {% endfor %}
</form>

Similarly, if the formset has the ability to order (can_order=True), it is possible to render it with {{ form.ORDER }}.

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.


from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import ArticleForm

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(request, '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 %}
        {{ form }}
        {% endfor %}
    </table>
</form>

However there’s a slight shortcut for the above by letting 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.

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.

Django-excel

github: django-excel.

A Django middleware to read, manipulate and write data in different excel formats: csv, ods, xls, xlsx and xlsm.


pip install django-excel

FILE_UPLOAD_HANDLERS = (
    "django_excel.ExcelMemoryFileUploadHandler",
    "django_excel.TemporaryExcelFileUploadHandler")

###############

from django.shortcuts import render_to_response
from django.http import HttpResponseBadRequest
from django import forms
from django.template import RequestContext
import django_excel as excel

class UploadFileForm(forms.Form):
    file = forms.FileField()

def upload(request):
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            filehandle = request.FILES['file']
            return excel.make_response(filehandle.get_sheet(), "csv")
        else:
            return HttpResponseBadRequest()
    else:
        form = UploadFileForm()
    return render_to_response('upload_form.html',
                              {'form': form},
                              context_instance=RequestContext(request))

def download(request):
    sheet = excel.pe.Sheet([[1, 2],[3, 4]])
    return excel.make_response(sheet, "csv")