Formsets

Un formset est une couche d’abstraction pour travailler avec plusieurs formulaires sur la même page. On peut opportunément le comparer à une grille de données (data grid). Supposons que vous ayez le formulaire suivant:

>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()

Vous voudrez sans doute que l’utilisateur puisse créer plusieurs articles à la fois. Pour créer un formset à partir d’un ArticleForm vous ferez:

>>> from django.forms.formsets import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)

Vous venez de créer un formset nommé ArticleFormSet. Le formset vous donne la capacité de parcourir les formulaires du formset et de les afficher comme vous le feriez avec un formulaire ordinaire:

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

Comme vous pouvez le constater, il n’y a qu’un seul formulaire vide d’affiché. Le nombre de formulaires vides à afficher est contrôlé par le paramètre extra. Par défaut, formset_factory déclare un formulaire supplémentaire; l’exemple suivant affichera deux formulaires vierges:

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)

Avant Django 1.3, les instances formset n’étaient pas itérables. Pour produire le formset, vous itériez à travers l’attribut forms:

>>> formset = ArticleFormSet()
>>> for form in formset.forms:
...     print form.as_table()

Itérer à travers formset.forms produira les formulaires dans l’ordre de leur création. L’itérateur de formset par défaut produit également les formulaires dans cet ordre, mais vous pouvez le modifier en fournissant une implémentation alternative de la méthode __iter__().

Les formsets peuvent également être appelés par indexes, ce qui renverra le formulaire correspondant. Si vous surchargez __iter__, vous devrez aussi surcharger __getitem__ pour avoir le même comportement.

Utiliser des données initiales dans un formset

Les données initiales sont ce qui motivent la principale utilisation d’un formset. Comme indiqué précédemment, vous pouvez définir le nombre de formulaires supplémentaires. Ce que cela veut dire, c’est que vous dites au formset combien il y a de formulaires supplémentaires à afficher en plus des formulaires qu’il a généré à partir des données initiales. Voyons cet exemple:

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

Il y a maintenant un total de trois formulaires. Un pour les données initiales passées et deux formulaires supplémentaires. Notez également que nous avons passé un dictionnaire comme données initiales.

Limiter le nombre maximum de formulaires

Le paramètre max_num de formset_factory vous permet d’indiquer le nombre maximum de formulaires vides que le formset affichera:

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

Si la valeur de max_num est supérieur au nombre d’objets existant, des formulaires vierges seront ajoutés au formset, à concurrence de la valeur extra, sans que le nombre total de formulaires excède max_num.

Une valeur max_num à None (la valeur par défaut) enlève toute limite au nombre de formulaires affichés. Notez que la valeur par défaut de max_num a été changé de 0 à None dans la version 1.2 pour permettre 0 comme valeur valide.

Validation du Formset

La validation d’un formset est pratiquement identique à celle d’un Form ordinaire. Une méthode is_valid du formset fournit un moyen pratique pour valider tous les formulaires du 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

donne ceci ? :

Nous n’avons pas passé de données au formset, ce qui aboutit à un formulaire valide. Le formset est assez intelligent pour ignorer les formulaires supplémentaires qui n’ont pas été modifiés. Si nous fournissons un article invalide:

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

Comme vous pouvez le voir, formset.errors est une liste dont les entrées correspondent aux formulaires du formset. La validation a été effectuée pour chacun des deux formulaires, et le message d’erreur attendu apparaît pour le deuxième élément.

Nous pouvons aussi vérifier si les données du formulaire diffèrent des données initiales (c’est à dire si le formulaire a été envoyé sans aucune donnée):

>>> data = {
...     'form-TOTAL_FORMS': u'1',
...     'form-INITIAL_FORMS': u'0',
...     'form-MAX_NUM_FORMS': u'',
...     'form-0-title': u'',
...     'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False

Comprendre ManagementForm

Vous aurez remarqué les données additionnelles (form-TOTAL_FORMS, form-INITIAL_FORMS et form-MAX_NUM_FORMS) requises dans les données des formsets précédent. Ces données sont requises pour le ManagementForm. Ce formulaire ManagementForm est utilisé par le formset pour gérer la collection de formulaires contenus dans le formset. Si vous ne fournissez pas ces données de gestionnaire, une exception sera levée:

>>> 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']

Il est utilisé pour garder une trace du nombre d’instances en cours d’affichage. Si vous ajoutez de nouveaux formulaires via JavaScript, vous devriez aussi incrémenter le champ décompte de ce formulaire.

Le formulaire management est disponible en tant qu’attribut du formset lui-même. Lorsqu’un formset est mis en forme dans un template, vous pouvez inclure toutes les données de gestion avec``{{ my_formset.management_form }}`` (en substituant le nom de votre formset).

total_form_count et initial_form_count

BaseFormSet a quelques méthodes étroitement liées à ManagementForm, total_form_count et initial_form_count.

total_form_count renvoie le nombre total de formulaires dans ce formset. initial_form_count renvoie le nombre de formulaires dans le formset qui ont été pré-remplis, et il est également utilisé pour déterminer combien de formulaires sont requis. Vous n’aurez probablement jamais besoin de surcharger ces méthodes, alors si vous le faites, soyez sûrs de comprendre ce que vous faites.

empty_form

BaseFormSet fournit un attribut additionnel empty_form qui renvoie une instance de formulaire avec un préfixe __prefix__ pour faciliter l’utilisation de formulaires dynamiques avec JavaScript.

Validation de formset personnalisé

Un formset a une méthode clean semblable à celle de la classe Form. C’est là que vous définissez votre propre validation, qui travaille au niveau du formset:

>>> 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.']

La méthode de formset clean est appelée après que toutes les méthodes Form.clean aient été appelées. Les erreurs peuvent être trouvées en utilisant la méthode non_form_errors() du formset.

Traiter le tri et la suppression de formulaires

Une situation courante avec le formset est le tri et la suppression des instances de formulaires. On s’en occupe pour vous. Le formset_factory fournit deux paramètres facultatifs can_order et can_delete qui s’occupent d’ajouter les champs supplémentaires et fournir un moyen d’accès simple à ces données.

can_order

Default: False

Vous permet la création d’un formset avec possibilités de tri:

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

Il ajoute un champ additionnel à chaque formulaire. Ce nouveau champ est nommé ORDER et c’est un forms.IntegerField. Pour les formulaires qui viennent des données initiales, il assigne automatiquement une valeur numérique. Voyons ce qui se passe lorsqu’un utilisateur modifie ces valeurs:

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

Vous permet la création d’un formset avec possibilités de suppression:

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

Semblable à can_order, ajoute un nouveau champ à chaque formulaire nommé DELETE et c’est un forms.BooleanField. –> !!! When data comes through marking any of the delete fields you can access them with deleted_forms !!! <– Phrase non traduite au risque de contresens...:

>>> 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'}]

Ajouter des champs additionnels à un formset

Si vous avez besoin d’ajouter des champs additionnels au formset, cela se fait facilement. La classe de base formset fournit une méthode add_fields. Il vous suffit de surcharger cette méthode pour ajouter vos propres champs ou même redéfinir les champs/attributs par défaut des champs tri et suppression:

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

Utiliser un formset dans les vues et les templates

Utiliser un formset dans une vue est aussi facile qu’utiliser une class Form ordinaire. La seule chose à laquelle vous devrez être attentif est d’utiliser le formulaire management dans le template. Regardons une vue:

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

Le template manage_articles.html ressemblera à ceci:

<form method="post" action="">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>

Mais on peut raccourcir ce qui précède et laisser le formset lui-même se débrouiller avec le formulaire management:

<form method="post" action="">
    <table>
        {{ formset }}
    </table>
</form>

Cet exemple finit par appeler la méthode as_table de la classe formset.

can_delete et can_order fournis manuellement

Si vous fournissez manuellement les champs dans le template, vous pouvez utiliser {{ form.DELETE }} pour fournir le paramètre can_delete:

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

De même, si le formset a la capacité de trier (can_order=True), il est possible de le produire avec {{ form.ORDER }}.

Utiliser plus d’un formset dans une vue

Si vous le souhaitez, vous pouvez utiliser plus d’un formset dans une vue. les formsets empruntent beaucoup de leurs comportements aux formulaires. Cela étant dit, vous pouvez utiliser un prefix pour préfixer les noms des champs du formulaire du formset avec une valeur donnée, pour envoyer plusieurs formsets à une vue sans provoquer de conflits. Voyons comment on peut faire ça:

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,
    })

Vous produirez les formsets comme d’habitude. Il est important de souligner que vous devez passer le prefix dans les cas POST comme pour les cas non-POST, pour qu’il soit rendu et traité correctement.

Contact