# Documentation Django 6.0.7

# Formulaires groupés

class BaseFormSet

Les formulaires groupés sont une couche d’abstraction pour travailler avec plusieurs formulaires sur une même page. On peut comparer cela à un tableau de données. Admettons que l’on dispose du formulaire suivant :

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

Il peut être souhaitable de permettre à l’utilisateur de créer plusieurs articles à la fois. Pour créer des formulaires groupés à partir d’un formulaire `ArticleForm`, voici comment procéder :

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

Vous avez maintenant créé une classe de formulaires groupés nommée `ArticleFormSet`. En créant une instance de cette classe, cela vous donne la possibilité d’effectuer une itération sur les formulaires du groupe et de les afficher tout comme vous le feriez pour un formulaire habituel :

    >>> formset = ArticleFormSet()
    >>> for form in formset:
    ...     print(form)
    ...
    <div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
    <div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>

Comme vous le voyez, un seul formulaire vierge a été affiché. Le nombre de formulaires vierges affichés dépend du paramètre `extra`. Par défaut, `formset_factory()` définit un seul formulaire supplémentaire ; l’exemple suivant crée une classe de formulaires groupés pour afficher deux formulaires vierges :

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

Formsets can be iterated and indexed, accessing forms in the order they were created. You can reorder the forms by overriding the default `__iter__()` and `__getitem__()` methods if needed. (For more information on implementing these methods, see the Python documentation on sequences.)

## Données initiales pour les formulaires groupés

Les données initiales sont le moteur essentiel qui fait l’intérêt des formulaires groupés. Comme on peut le voir ci-dessus, vous pouvez définir le nombre de formulaires supplémentaires. Cela signifie que vous pouvez indiquer aux formulaires groupés combien de formulaires supplémentaires doivent être affichés en plus des formulaires générés à partir des données initiales. Examinons un exemple plus en détails :

    >>> 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)
    ...
    <div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title"></div>
    <div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2023-02-11" id="id_form-0-pub_date"></div>
    <div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" id="id_form-1-title"></div>
    <div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" id="id_form-1-pub_date"></div>
    <div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
    <div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>

Nous avons maintenant trois formulaires qui s’affichent. Un pour les données initiales que nous lui avons transmises et deux formulaires supplémentaires. Notez également que nous transmettons une liste de dictionnaires comme données initiales.

SI vous utilisez `initial` dans l’affichage de formulaires groupés, vous devez passer la même valeur `initial` lors du traitement de ces formulaires au moment de l’envoi des données pour permettre la détection des formulaires modifiés par l’utilisateur. Par exemple, vous pourriez avoir quelque chose comme : `ArticleFormSet(request.POST, initial=[...])`.

Voir aussi

Création de formulaires groupés à partir de modèles avec les formulaires groupés de modèle.

## Restriction du nombre maximum de formulaires

Le paramètre `max_num` de `formset_factory()` donne la possibilité de restreindre le nombre de formulaires que les formulaires groupés vont afficher :

    >>> from django.forms import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
    >>> formset = ArticleFormSet()
    >>> for form in formset:
    ...     print(form)
    ...
    <div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
    <div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>

Si la valeur de `max_num` est plus grande que le nombre d’objets existants dans les données initiales, un maximum de `extra` formulaires vierges supplémentaires seront ajoutés au formulaire groupé, tant que le nombre total de formulaires n’excède pas `max_num`. Par exemple, si `extra=2` et `max_num=2` et que le formulaire groupé est initialisé avec un élément `initial`, on verra s’afficher un formulaire pour l’élément initial et un formulaire vierge.

Si le nombre d’éléments dans les données initiales dépasse `max_num`, tous les formulaires correspondant aux données initiales seront affichés quelle que soit la valeur de `max_num` et aucun formulaire supplémentaire ne sera affiché. Par exemple, si `extra=3` et `max_num=1` et que le formulaire groupé est initialisé avec deux éléments, ce sont deux formulaires avec les données initiales qui seront affichés.

Une valeur `max_num` `None` (par défaut) place une limite élevée du nombre de formulaires affichés (1000). En pratique, cela équivaut à aucune limite.

Par défaut, `max_num` ne s’applique qu’au nombre de formulaires affichés et non pas à la validation. Si `validate_max=True` est transmis à `formset_factory()`, `max_num` s’applique alors à la validation. Voir validate_max.

## Restriction du nombre maximum de formulaires instanciés

Le paramètre `absolute_max` de `formset_factory()` permet de limiter le nombre de formulaires pouvant être instanciés lors de la soumission de données `POST`. Ceci protège contre les attaques par épuisement de la mémoire qui utilisent des requêtes `POST` trafiquées :

    >>> from django.forms.formsets import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> ArticleFormSet = formset_factory(ArticleForm, absolute_max=1500)
    >>> data = {
    ...     "form-TOTAL_FORMS": "1501",
    ...     "form-INITIAL_FORMS": "0",
    ... }
    >>> formset = ArticleFormSet(data)
    >>> len(formset.forms)
    1500
    >>> formset.is_valid()
    False
    >>> formset.non_form_errors()
    ['Please submit at most 1000 forms.']

Lorsque `absolute_max` vaut `None`, la valeur par défaut est de `max_num + 1000` (si `max_num` vaut `None`, sa valeur par défaut est de `2000`).

Si `absolute_max` est plus petit que `max_num`, une erreur `ValueError` est générée.

## Validation des formulaires groupés

La validation des formulaires groupés est presque identique à celle d’un formulaire `Form` normal. L’objet de formulaires groupés contient une méthode `is_valid` afin de fournir une manière agile de valider tous les formulaires du groupe :

    >>> from django.forms import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> ArticleFormSet = formset_factory(ArticleForm)
    >>> data = {
    ...     "form-TOTAL_FORMS": "1",
    ...     "form-INITIAL_FORMS": "0",
    ... }
    >>> formset = ArticleFormSet(data)
    >>> formset.is_valid()
    True

Nous n’avons fourni aucune donnée aux formulaires groupés ce qui aboutit à un formulaire valide. Les formulaires groupés sont assez intelligents pour ne pas prendre en compte des formulaires supplémentaires qui n’ont pas été modifiés. Si nous passons un article non valable :

    >>> data = {
    ...     "form-TOTAL_FORMS": "2",
    ...     "form-INITIAL_FORMS": "0",
    ...     "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.']}]

Comme on peut le voir, `formset.errors` est une liste composée d’éléments correspondants à chaque formulaire du groupe. La validation s’est faite pour chacun des deux formulaires, et le message d’erreur attendu apparaît pour le second.

Tout comme pour l’utilisation de formulaires normaux, chaque champ de formulaire groupé peut inclure des attributs HTML tels que `maxlength` pour la validation par le navigateur. Cependant, les champs de formulaires groupés n’incluront pas l’attribut `required` car cette validation pourrait être incorrecte lors de l’ajout ou de la suppression de formulaires.

BaseFormSet.total_error_count()

Pour contrôler le nombre d’erreurs dans les formulaires groupés, nous pouvons utiliser la méthode `total_error_count`:

    >>> # Using the previous example
    >>> formset.errors
    [{}, {'pub_date': ['This field is required.']}]
    >>> len(formset.errors)
    2
    >>> formset.total_error_count()
    1

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

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

### Rôle du formulaire de gestion (`ManagementForm`)

Vous avez peut-être remarqué les données supplémentaires (`form-TOTAL_FORMS`, `form-INITIAL_FORMS`) qui étaient requises dans les données de formulaires groupés ci-dessus. Ces données sont exigées par le formulaire de gestion. Celui-ci est utilisé par les formulaires groupés pour gérer l’ensemble des formulaires contenus dans le groupe. Si vous ne fournissez pas ces données de gestion, le jeu de formulaires ne sera pas valide :

    >>> data = {
    ...     "form-0-title": "Test",
    ...     "form-0-pub_date": "",
    ... }
    >>> formset = ArticleFormSet(data)
    >>> formset.is_valid()
    False

Ces données servent à conserver la trace du nombre d’instances de formulaires qui sont affichées. Si vous ajoutez de nouveaux formulaires par JavaScript, il est aussi nécessaire d’incrémenter les champs de comptage de ce formulaire. D’un autre côté, si vous utilisez JavaScript pour permettre la suppression d’objets existants, vous devez alors vous assurer que ceux qui sont supprimés soient correctement marqués comme « à supprimer » en incluant le paramètre `form-#-DELETE` dans les données d’envoi `POST`. Il est prévu que tous les formulaires soient présents dans les données `POST` quoi qu’il arrive.

Le formulaire de gestion est disponible sous forme d’attribut de l’objet de formulaires groupés. Lorsque vous affichez des formulaires groupés dans un gabarit, vous pouvez inclure toutes les données de gestion en affichant `{{ my_formset.management_form }}` (en remplaçant le nom de vos formulaires groupés en conséquence).

Note

En plus des champs `form-TOTAL_FORMS` et `form-INITIAL_FORMS` montrés dans ces exemples, le formulaire de gestion inclut aussi les champs `form-MIN_NUM_FORMS` et `form-MAX_NUM_FORMS`. Ils sont produits avec le reste du formulaire de gestion, mais seulement utiles pour le code côté client. Ces champs ne sont pas obligatoires et ne sont donc pas affichés dans l’exemple de données `POST`.

### `total_form_count` et `initial_form_count`

`BaseFormSet` possède quelques méthodes étroitement liées aux données `total_form_count` et `initial_form_count` du formulaire de gestion `ManagementForm`.

`total_form_count` renvoie le nombre total de formulaires du groupe de formulaires. `initial_form_count` renvoie le nombre de formulaires du groupe qui ont été pré-remplis et est également utilisé pour déterminer le nombre de formulaires requis. Vous n’aurez probablement jamais à surcharger l’une de ces méthodes, mais prenez garde à bien comprendre ce qu’elles font si vous deviez le faire.

### `empty_form`

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

### `error_messages`

L’argument `error_messages` vous permet de surcharger les messages par défaut que le jeu de formulaires va produire. Passez un dictionnaire dont les clés correspondent aux messages d’erreur que vous souhaitez surcharger. Ces clés sont : `'too_few_forms'`, `'too_many_forms'` et `'missing_management_form'`. Les messages d’erreur `'too_few_forms'` et `'too_many_forms'` peuvent contenir `%(num)d`, ce qui sera remplacé respectivement par `min_num` et `max_num`.

Par exemple, voici le message d’erreur par défaut lorsque le formulaire de gestion est manquant :

    >>> formset = ArticleFormSet({})
    >>> formset.is_valid()
    False
    >>> formset.non_form_errors()
    ['ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. You may need to file a bug report if the issue persists.']

Et voici un message d’erreur personnalisé :

    >>> formset = ArticleFormSet(
    ...     {}, error_messages={"missing_management_form": "Sorry, something went wrong."}
    ... )
    >>> formset.is_valid()
    False
    >>> formset.non_form_errors()
    ['Sorry, something went wrong.']

### Validation personnalisée des formulaires groupés

Les formulaires groupés ont une méthode `clean` similaire à celle d’une classe `Form`. C’est là que vous pouvez définir votre propre validation agissant au niveau des formulaires groupés :

    >>> from django.core.exceptions import ValidationError
    >>> 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 = set()
    ...         for form in self.forms:
    ...             if self.can_delete and self._should_delete_form(form):
    ...                 continue
    ...             title = form.cleaned_data.get("title")
    ...             if title in titles:
    ...                 raise ValidationError("Articles in a set must have distinct titles.")
    ...             titles.add(title)
    ...

    >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
    >>> data = {
    ...     "form-TOTAL_FORMS": "2",
    ...     "form-INITIAL_FORMS": "0",
    ...     "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.']

La méthode `clean` des formulaires groupés est appelée après les méthodes `Form.clean` des formulaires individuels. Les erreurs produites à ce niveau sont accessibles par la méthode `non_form_errors()` sur l’objet des formulaires groupés.

Les erreurs non liées au formulaire sont produites avec une classe supplémentaire `nonform` pour aider à les distinguer des autres erreurs spécifiques au formulaire. Par exemple, `{{ formset.non_form_errors }}` donnera quelque chose comme :

    <ul class="errorlist nonform">
        <li>Articles in a set must have distinct titles.</li>
    </ul>

## Validation du nombre de formulaires dans les formulaires groupés

Django fournit plusieurs manières de valider le nombre minimum ou maximum de formulaires soumis. Les applications ayant besoin d’une validation plus adaptée du nombre de formulaires peuvent utiliser la validation personnalisée de formulaires groupés.

### `validate_max`

Si `validate_max=True` est transmis à `formset_factory()`, la validation vérifiera également que le nombre de formulaires dans les données reçues moins ceux marqués pour la suppression ne dépasse pas `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-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 at most 1 form.']

`validate_max=True` valide strictement la valeur de `max_num`, même si cette valeur a été dépassée en raison d’un nombre de données initiales trop important.

Le message d’erreur peut être personnalisé en passant le message `'too_many_forms'` dans l’argument error_messages.

Note

Quelle que soit la valeur de `validate_max`, si le nombre de formulaires dans un jeu de données dépasse `absolute_max`, la validation du formulaire échoue comme si `validate_max` avait été défini ; de plus, seuls les `absolute_max` premiers formulaires seront validés. Tout le reste est purement ignoré. Il s’agit d’une mesure de protection contre les attaques de remplissage de mémoire employant des requêtes `POST` contrefaites. Voir Restriction du nombre maximum de formulaires instanciés.

### `validate_min`

Si `validate_min=True` est passé à `formset_factory()`, la validation vérifiera également que le nombre de formulaires dans les données reçues moins ceux marqués pour la suppression est supérieur ou égal à `min_num`.

    >>> from django.forms import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
    >>> data = {
    ...     "form-TOTAL_FORMS": "2",
    ...     "form-INITIAL_FORMS": "0",
    ...     "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 at least 3 forms.']

Le message d’erreur peut être personnalisé en passant le message `'too_few_forms'` dans l’argument error_messages.

Note

Quelle que soit la valeur de `validate_min`, si un jeu de formulaires ne contient aucune donnée, `extra + min_num` formulaires vides seront affichés.

## Tri et suppression de formulaires

La méthode `formset_factory()` fournit deux paramètres facultatifs `can_order` et `can_delete` pour faciliter le tri et la suppression de formulaires dans des formulaires groupés.

### `can_order`

BaseFormSet.can_order

Valeur par défaut : `False`

Ce paramètre permet de créer des formulaires groupés qui peuvent être triés :

    >>> from 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)
    ...
    <div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
    <div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
    <div><label for="id_form-0-ORDER">Order:</label><input type="number" name="form-0-ORDER" value="1" id="id_form-0-ORDER"></div>
    <div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
    <div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
    <div><label for="id_form-1-ORDER">Order:</label><input type="number" name="form-1-ORDER" value="2" id="id_form-1-ORDER"></div>
    <div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
    <div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
    <div><label for="id_form-2-ORDER">Order:</label><input type="number" name="form-2-ORDER" id="id_form-2-ORDER"></div>

Ceci ajoute un champ supplémentaire à chaque formulaire. Ce nouveau champ s’appelle `ORDER` et est de type `forms.IntegerField`. Pour les formulaires générés à partir des données initiales, ces champs reçoivent automatiquement une valeur numérique. Voyons ce qui se passe si l’utilisateur modifie ces valeurs :

    >>> data = {
    ...     "form-TOTAL_FORMS": "3",
    ...     "form-INITIAL_FORMS": "2",
    ...     "form-0-title": "Article #1",
    ...     "form-0-pub_date": "2008-05-10",
    ...     "form-0-ORDER": "2",
    ...     "form-1-title": "Article #2",
    ...     "form-1-pub_date": "2008-05-11",
    ...     "form-1-ORDER": "1",
    ...     "form-2-title": "Article #3",
    ...     "form-2-pub_date": "2008-05-01",
    ...     "form-2-ORDER": "0",
    ... }

    >>> formset = ArticleFormSet(
    ...     data,
    ...     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.ordered_forms:
    ...     print(form.cleaned_data)
    ...
    {'title': 'Article #3', 'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0}
    {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1}
    {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2}

Brut `BaseFormSet` fournit également un attribut `ordering_widget` et une méthode `get_ordering_widget()` qui permet de contrôler le composant utilisé avec `can_order`.

#### `ordering_widget`

BaseFormSet.ordering_widget

Par défaut : `NumberInput`

Définissez `ordering_widget` pour indiquer la classe de composant à utiliser avec `can_order`:

    >>> from django.forms import BaseFormSet, formset_factory
    >>> from myapp.forms import ArticleForm
    >>> class BaseArticleFormSet(BaseFormSet):
    ...     ordering_widget = HiddenInput
    ...

    >>> ArticleFormSet = formset_factory(
    ...     ArticleForm, formset=BaseArticleFormSet, can_order=True
    ... )

#### `get_ordering_widget`

BaseFormSet.get_ordering_widget()

Surchargez `get_ordering_widget()` si vous avez besoin de fournir une instance de composant à utiliser avec `can_order`:

    >>> from django.forms import BaseFormSet, formset_factory
    >>> from myapp.forms import ArticleForm
    >>> class BaseArticleFormSet(BaseFormSet):
    ...     def get_ordering_widget(self):
    ...         return HiddenInput(attrs={"class": "ordering"})
    ...

    >>> ArticleFormSet = formset_factory(
    ...     ArticleForm, formset=BaseArticleFormSet, can_order=True
    ... )

### `can_delete`

BaseFormSet.can_delete

Valeur par défaut : `False`

Ce paramètre permet de créer des formulaires groupés où l’on peut choisir des formulaires à supprimer :

    >>> from django.forms import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> ArticleFormSet = formset_factory(ArticleForm, can_delete=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)
    ...
    <div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
    <div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
    <div><label for="id_form-0-DELETE">Delete:</label><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE"></div>
    <div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
    <div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
    <div><label for="id_form-1-DELETE">Delete:</label><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE"></div>
    <div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
    <div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
    <div><label for="id_form-2-DELETE">Delete:</label><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE"></div>

Un peu comme pour `can_order`, ce paramètre ajoute un nouveau champ `DELETE` de type `forms.BooleanField` à chaque formulaire. Lorsque les données reviennent et que certains de ces champs ont une valeur « vrai », il est possible d’accéder aux formulaires concernés avec `deleted_forms`:

    >>> data = {
    ...     "form-TOTAL_FORMS": "3",
    ...     "form-INITIAL_FORMS": "2",
    ...     "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]
    [{'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'DELETE': True}]

Si vous utilisez un `ModelFormSet`, les instances de modèle correspondant aux formulaires supprimés seront supprimées lorsque `formset.save ()` sera appelée.

Si vous appelez `formset.save(commit=False)`, les objets ne seront pas supprimés automatiquement. Vous devrez appeler `delete()` pour chacun des `formset.deleted_objects` afin que la suppression soit effective :

    >>> instances = formset.save(commit=False)
    >>> for obj in formset.deleted_objects:
    ...     obj.delete()
    ...

D’autre part, si vous utilisez un `FormSet` simple, c’est à vous de gérer `formset.deleted_forms`, par exemple dans la méthode `save()` de votre formulaire groupé, car il n’existe pas de notion générale sur ce qu’implique la suppression d’un formulaire.

`BaseFormSet` fournit également un attribut `deletion_widget` et une méthode `get_deletion_widget()` qui permet de contrôler le composant utilisé avec `can_delete`.

#### `deletion_widget`

BaseFormSet.deletion_widget

Par défaut : `CheckboxInput`

Définissez `deletion_widget` pour indiquer la classe de composant à utiliser avec `can_delete`:

    >>> from django.forms import BaseFormSet, formset_factory
    >>> from myapp.forms import ArticleForm
    >>> class BaseArticleFormSet(BaseFormSet):
    ...     deletion_widget = HiddenInput
    ...

    >>> ArticleFormSet = formset_factory(
    ...     ArticleForm, formset=BaseArticleFormSet, can_delete=True
    ... )

#### `get_deletion_widget`

BaseFormSet.get_deletion_widget()

Surchargez `get_deletion_widget()` si vous avez besoin de fournir une instance de composant à utiliser avec `can_delete`:

    >>> from django.forms import BaseFormSet, formset_factory
    >>> from myapp.forms import ArticleForm
    >>> class BaseArticleFormSet(BaseFormSet):
    ...     def get_deletion_widget(self):
    ...         return HiddenInput(attrs={"class": "deletion"})
    ...

    >>> ArticleFormSet = formset_factory(
    ...     ArticleForm, formset=BaseArticleFormSet, can_delete=True
    ... )

### `can_delete_extra`

BaseFormSet.can_delete_extra

Valeur par défaut : `True`

Lorsque `can_delete=True`, la définition de `can_delete_extra=False` enlève la possibilité de supprimer les formulaires supplémentaires.

## Ajout de champs supplémentaires à des formulaires groupés

L’ajout de champs supplémentaires aux formulaires groupés est simple à effectuer. La classe de base des formulaires groupés offre une méthode `add_fields`. Vous pouvez surcharger cette méthode pour ajouter vos propres champs ou même pour redéfinir les champs/attributs par défaut des champs de tri et de suppression :

    >>> from django.forms import BaseFormSet
    >>> from django.forms import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> class BaseArticleFormSet(BaseFormSet):
    ...     def add_fields(self, form, index):
    ...         super().add_fields(form, index)
    ...         form.fields["my_field"] = forms.CharField()
    ...

    >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
    >>> formset = ArticleFormSet()
    >>> for form in formset:
    ...     print(form)
    ...
    <div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
    <div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>
    <div><label for="id_form-0-my_field">My field:</label><input type="text" name="form-0-my_field" id="id_form-0-my_field"></div>

## Transmission de paramètres personnalisés aux formulaires de jeux de formulaires

Il arrive parfois qu’une classe de formulaire accepte des paramètres personnalisés, comme `MyArticleForm`. Vous pouvez transmettre ce paramètre lors de l’instanciation du jeu de formulaires :

    >>> from django.forms import BaseFormSet
    >>> from django.forms import formset_factory
    >>> from myapp.forms import ArticleForm

    >>> class MyArticleForm(ArticleForm):
    ...     def __init__(self, *args, user, **kwargs):
    ...         self.user = user
    ...         super().__init__(*args, **kwargs)
    ...

    >>> ArticleFormSet = formset_factory(MyArticleForm)
    >>> formset = ArticleFormSet(form_kwargs={"user": request.user})

Le paramètre `form_kwargs` peut aussi dépendre de l’instance de formulaire spécifique. La classe de base du jeu de formulaires fournit une méthode `get_form_kwargs`. Celle-ci accepte un seul paramètre, l’indice du formulaire dans le jeu de formulaire. Cet indice vaut `None` pour le formulaire vide empty_form:

    >>> from django.forms import BaseFormSet
    >>> from django.forms import formset_factory

    >>> class BaseArticleFormSet(BaseFormSet):
    ...     def get_form_kwargs(self, index):
    ...         kwargs = super().get_form_kwargs(index)
    ...         kwargs["custom_kwarg"] = index
    ...         return kwargs
    ...

    >>> ArticleFormSet = formset_factory(MyArticleForm, formset=BaseArticleFormSet)
    >>> formset = ArticleFormSet()

## Personnalisation du préfixe d’un jeu de formulaires

Dans le rendu HTML, les jeux de formulaires ajoutent un préfixe devant chaque nom de champ. Par défaut, le préfixe est `'form'`, mais il peut être personnalisé à l’aide du paramètre `prefix` du jeu de formulaires.

Par exemple, dans le cas par défaut, on pourrait voir :

    <label for="id_form-0-title">Title:</label>
    <input type="text" name="form-0-title" id="id_form-0-title">

Mais avec `ArticleFormset(prefix='article')`, cela deviendra :

    <label for="id_article-0-title">Title:</label>
    <input type="text" name="article-0-title" id="id_article-0-title">

Ceci est utile lorsqu’on veut utiliser plus d’un jeu de formulaires dans une vue.

## Utilisation des formulaires groupés dans les vues et les gabarits

Les jeux de formulaires possèdent les attributs et méthodes suivantes associées à leur rendu :

BaseFormSet.renderer

  Indique le moteur de rendu à utiliser pour le jeu de formulaires. Contient par défaut le moteur de rendu défini dans le réglage `FORM_RENDERER`.

BaseFormSet.template_name

  Le nom du gabarit produit si le jeu de formulaire est forcé à une chaîne, par ex. via `print(formset)` ou dans un gabarit via `{{ formset }}`.

  Par défaut, une propriété renvoyant la valeur `formset_template_name` du producteur. Vous pouvez la définir à un nom de gabarit afin de la surcharger pour une classe de jeu de formulaires particulière.

  Ce gabarit sera utilisé pour produire le formulaire de gestion du jeu de formulaires, puis chaque formulaire du jeu en respectant le gabarit défini par l’attribut `template_name` du formulaire.

BaseFormSet.template_name_div

  Le nom du gabarit à utiliser lors de l’appel à `as_div()`. Par défaut, il s’agit de `'django/forms/formsets/div.html'`. Ce gabarit produit le formulaire de gestion du jeu de formulaires, puis chaque formulaire du jeu en respectant la méthode `as_div()` du formulaire.

BaseFormSet.template_name_p

  Le nom du gabarit à utiliser lors de l’appel à `as_p()`. Par défaut, il s’agit de `"django/forms/formsets/p.html"`. Ce gabarit produit le formulaire de gestion du jeu de formulaires, puis chaque formulaire du jeu en respectant la méthode `as_p()` du formulaire.

BaseFormSet.template_name_table

  Le nom du gabarit à utiliser lors de l’appel à `as_table()`. Par défaut, il s’agit de `"django/forms/formsets/table.html"`. Ce gabarit produit le formulaire de gestion du jeu de formulaires, puis chaque formulaire du jeu en respectant la méthode `as_table()` du formulaire.

BaseFormSet.template_name_ul

  Le nom du gabarit à utiliser lors de l’appel à `as_ul()`. Par défaut, il s’agit de `"django/forms/formsets/ul.html"`. Ce gabarit produit le formulaire de gestion du jeu de formulaires, puis chaque formulaire du jeu en respectant la méthode `as_ul()` du formulaire.

BaseFormSet.get_context()

  Renvoie le contexte pour la production du jeu de formulaires dans un gabarit.

  Le contexte disponible est :

  - `formset`: l’instance du jeu de formulaire.

BaseFormSet.render(template_name=None, context=None, renderer=None)

  La méthode de production est appelée par `__str__` ainsi que par les méthodes `as_div()`, `as_p()`, `as_ul()` et `as_table()`. Tous les arguments sont facultatifs et valent par défaut :

  - `template_name`: `template_name`

  - `context`: la valeur renvoyée par `get_context()`

  - `renderer`: la valeur renvoyée par `renderer`

BaseFormSet.as_div()

  Produit le jeu de formulaires avec le gabarit `template_name_div`.

BaseFormSet.as_p()

  Produit le jeu de formulaires avec le gabarit `template_name_p`.

BaseFormSet.as_table()

  Produit le jeu de formulaires avec le gabarit `template_name_table`.

BaseFormSet.as_ul()

  Produit le jeu de formulaires avec le gabarit `template_name_ul`.

L’utilisation de formulaires groupés dans une vue n’est pas très différent de l’utilisation d’une classe `Form` habituelle. La seule chose à laquelle il faut être attentif est de ne pas oublier d’inclure le formulaire de gestion dans le gabarit. Examinons un exemple de vue :

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

Le gabarit `manage_articles.html` pourrait ressembler à ceci :

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

Toutefois, le code ci-dessus peut être légèrement raccourci en laissant les formulaires groupés se charger eux-mêmes du formulaire de gestion :

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

Le code ci-dessus aboutit à l’appel de la méthode `BaseFormSet.render()` de la classe du jeu de formulaires. Ceci produit le jeu de formulaires en utilisant le gabarit défini par l’attribut `template_name`. Tout comme pour les formulaires, le jeu de formulaires est produit avec `as_div` par défaut, mais les méthodes utilitaires `as_p`, `as_ul` et `as_table` sont aussi disponibles. La production du jeu de formulaires peut être personnalisée en spécifiant l’attribut `template_name` ou plus généralement en surchargeant le gabarit par défaut.

### Affichage manuel de `can_delete` et `can_order`

Si vous affichez manuellement les champs dans le gabarit, vous pouvez afficher le paramètre `can_delete` avec `{{ form.DELETE }}`:

    <form method="post">
        {{ 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>

De la même façon, si les formulaires groupés peuvent être triés (`can_order=True`), il est possible d’afficher le champ de tri avec `{{ form.ORDER }}`.

### Utilisation de plus d’un objet de formulaires groupés dans une vue

Il est possible d’utiliser plus d’un objet de formulaires groupés dans une vue. Les formulaires groupés ont un comportement très semblable à celui des formulaires. Ceci dit, vous pouvez utiliser l’attribut `prefix` afin de préfixer les noms de champs des formulaires groupés avec une valeur donnée, ce qui permettra d’envoyer plusieurs ensembles de formulaires groupés à une vue sans conflit de nommage. Voyons un peu comment cela pourrait être fait :

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

    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(
            request,
            "manage_articles.html",
            {
                "article_formset": article_formset,
                "book_formset": book_formset,
            },
        )

Les formulaires groupés seraient ensuite affichés comme d’habitude. Il est important de relever que vous devez indiquer `prefix` dans tous les cas (`POST` ou non), afin que l’affichage et le traitement des données puissent se faire correctement.

Chaque prefix de jeu de formulaires remplace le préfixe par défaut `form` qui est ajouté à chaque attribut HTML `name` et `id` des champs de formulaire.