Création de formulaires à partir de modèles

ModelForm

class ModelForm[source]

Si vous construisez une application basée sur une base de données, il y a des chances pour que vos formulaires correspondent étroitement avec les modèles Django. Par exemple, vous pourriez avoir un modèle BlogComment et vouloir créer un formulaire permettant d’envoyer des commentaires. Dans ce cas, il serait redondant de devoir définir les types de champs du formulaire, car vous avez déjà défini des champs au niveau du modèle.

C’est pour cette raison que Django fournit une classe utilitaire permettant de créer une classe de formulaire Form à partir d’un modèle Django.

Par exemple :

>>> from django.forms import ModelForm
>>> from myapp.models import Article

# Create the form class.
>>> class ArticleForm(ModelForm):
...     class Meta:
...         model = Article
...         fields = ['pub_date', 'headline', 'content', 'reporter']

# Creating a form to add an article.
>>> form = ArticleForm()

# Creating a form to change an existing article.
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)

Types de champs

La classe de formulaire générée contiendra un champ de formulaire pour chaque champ de modèle inclus, dans l’ordre indiqué par l’attribut fields.

Chaque champ de modèle possède un champ de formulaire par défaut. Par exemple, le champ CharField d’un modèle est représenté par un champ de formulaire CharField. Un champ ManyToManyField d’un modèle est représenté par un champ de formulaire MultipleChoiceField. Voici la liste complète des correspondances :

Champ de modèle Champ de formulaire
AutoField Non représenté dans le formulaire
BigAutoField Non représenté dans le formulaire
BigIntegerField IntegerField avec min_value à -9223372036854775808 et max_value à 9223372036854775807.
BooleanField BooleanField
CharField CharField with max_length set to the model field’s max_length and empty_value set to None if null=True.
CommaSeparatedIntegerField CharField
DateField DateField
DateTimeField DateTimeField
DecimalField DecimalField
EmailField EmailField
FileField FileField
FilePathField FilePathField
FloatField FloatField
ForeignKey ModelChoiceField (voir ci-dessous)
ImageField ImageField
IntegerField IntegerField
IPAddressField IPAddressField
GenericIPAddressField GenericIPAddressField
ManyToManyField ModelMultipleChoiceField (voir ci-dessous)
NullBooleanField NullBooleanField
PositiveIntegerField IntegerField
PositiveSmallIntegerField IntegerField
SlugField SlugField
SmallIntegerField IntegerField
TextField CharField avec widget=forms.Textarea
TimeField TimeField
URLField URLField

Comme l’on peut s’y attendre, les champs de modèle de type ForeignKey et ManyToManyField sont des cas spéciaux :

  • ForeignKey est représenté par django.forms.ModelChoiceField, qui est un champ ChoiceField dont les choix possibles sont définis par le QuerySet d’un modèle.
  • ManyToManyField est représenté par un django.forms.ModelMultipleChoiceField, qui est un champ MultipleChoiceField dont les choix possibles sont définis par le QuerySet d’un modèle.

De plus, chaque champ de formulaire généré possède les attributs comme suit :

  • Si le champ de modèle a blank=True, required est alors défini à False dans le champ de formulaire. Sinon, required=True.
  • L’étiquette (label) du champ de formulaire est défini à la valeur verbose_name du champ de modèle, avec le premier caractère en majuscules.
  • La valeur help_text du champ de formulaire est définie à la valeur help_text du champ de modèle.
  • Si la valeur choices d’un champ de modèle est définie, le composant ( widget) du champ de formulaire sera de type Select dont les choix proviendront de la valeur choices du champ de modèle. Ces choix incluent normalement le choix vierge qui est sélectionné par défaut. Si le champ est obligatoire, cela force l’utilisateur à faire un choix. Le choix vierge n’est pas inclus si le champ de modèle a blank=False ainsi qu’une valeur par défaut (default) explicite (c’est la valeur default qui sera initialement sélectionnée).

Pour terminer, notez que vous pouvez surcharger le champ de formulaire utilisé pour un champ de modèle donné. Consultez Surcharge des champs par défaut ci-dessous.

Un exemple complet

Considérez cet ensemble de modèles :

from django.db import models
from django.forms import ModelForm

TITLE_CHOICES = (
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
)

class Author(models.Model):
    name = models.CharField(max_length=100)
    title = models.CharField(max_length=3, choices=TITLE_CHOICES)
    birth_date = models.DateField(blank=True, null=True)

    def __str__(self):              # __unicode__ on Python 2
        return self.name

class Book(models.Model):
    name = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ['name', 'title', 'birth_date']

class BookForm(ModelForm):
    class Meta:
        model = Book
        fields = ['name', 'authors']

Avec ces modèles, les sous-classes ModelForm ci-dessus seraient à peu près équivalentes à ceci (avec comme seule différence la méthode save() que nous aborderons dans un moment) :

from django import forms

class AuthorForm(forms.Form):
    name = forms.CharField(max_length=100)
    title = forms.CharField(
        max_length=3,
        widget=forms.Select(choices=TITLE_CHOICES),
    )
    birth_date = forms.DateField(required=False)

class BookForm(forms.Form):
    name = forms.CharField(max_length=100)
    authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all())

Validation d’un ModelForm

La validation d’un ModelForm se distingue en deux étapes importantes :

  1. La validation du formulaire
  2. La validation de l’instance de modèle

Comme pour la validation de formulaire normale, la validation des formulaires de modèle est déclenchée implicitement lors de l’appel à is_valid() ou par l’accession à l’attribut errors, ou explicitement en appelant full_clean(), même si en pratique cette dernière méthode est rarement utilisée.

La validation de Model (Model.full_clean()) est déclenchée à l’intérieur de l’étape de validation de formulaire, juste après l’appel à la méthode clean() du formulaire.

Avertissement

Le processus de nettoyage modifie l’instance de modèle transmise au constructeur de ModelForm de plusieurs manières. Par exemple, toute valeur de champ date du modèle est convertie en vrai objet date. Une validation qui échoue peut laisser l’instance de modèle sous-jacente dans un état non cohérent et il n’est donc pas conseillé de la réutiliser.

Surcharge de la méthode clean()

Vous pouvez surcharger la méthode clean() d’un formulaire de modèle pour procéder à des validations supplémentaires de la même manière que pour un formulaire normal.

Une instance de formulaire de modèle liée à un objet modèle contient un attribut instance qui donne à ses méthodes l’accès à cette instance de modèle spécifique.

Avertissement

La méthode ModelForm.clean() définit un drapeau qui fait que l’étape de validation de modèle valide l’unicité des champs de modèle marqués comme unique, unique_together ou unique_for_date|month|year.

Si vous souhaitez surcharger la méthode clean() et maintenir cette validation, vous devez appeler la méthode clean() de la classe parente.

Interactions avec la validation des modèles

Dans le cadre du processus de validation, ModelForm appelle la méthode clean() de chaque champ du modèle possédant un champ de formulaire correspondant. Si vous avez exclu des champs de modèle, la validation ne sera pas effectuée pour ces champs. Consultez la documentation de la validation des formulaires pour en savoir plus sur le fonctionnement du nettoyage et de la validation des champs.

La méthode clean() du modèle est appelée avant les contrôles d’unicité. Consultez la validation des objets pour plus d’informations sur la méthode clean() des modèles.

Considérations sur les messages d’erreur des modèles

Les messages d’erreur définis au niveau des champs de formulaire ou au niveau de la classe Meta de formulaire ont toujours la priorité sur les messages d’erreur définis au niveau des champs de modèle.

Les messages d’erreur définis au niveau des champs de modèle ne sont utilisés que lorsqu’une erreur ValidationError est générée à l’étape de validation du modèle et qu’aucun message d’erreur correspondant n’est défini au niveau du formulaire.

Vous pouvez surcharger les messages d’erreur issus de NON_FIELD_ERRORS et générés par la validation des modèles en ajoutant la clé NON_FIELD_ERRORS au dictionnaire error_messages de la classe Meta interne des classes ModelForm:

from django.forms import ModelForm
from django.core.exceptions import NON_FIELD_ERRORS

class ArticleForm(ModelForm):
    class Meta:
        error_messages = {
            NON_FIELD_ERRORS: {
                'unique_together': "%(model_name)s's %(field_labels)s are not unique.",
            }
        }

La méthode save()

Chaque ModelForm possède aussi une méthode save(). Celle-ci crée et enregistre un objet en base de données à partir des données saisies dans le formulaire. Une sous-classe de ModelForm peut accepter une instance existante de modèle par le paramètre nommé instance. Si celui-ci est présent, save() met à jour cette instance. Dans le cas contraire, save() crée une nouvelle instance du modèle concerné :

>>> from myapp.models import Article
>>> from myapp.forms import ArticleForm

# Create a form instance from POST data.
>>> f = ArticleForm(request.POST)

# Save a new Article object from the form's data.
>>> new_article = f.save()

# Create a form to edit an existing Article, but use
# POST data to populate the form.
>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()

Notez que si le formulaire n’a pas été validé, l’appel à save() s’en occupera en contrôlant form.errors. Une erreur ValueError est générée si les données du formulaire ne sont pas valides, par exemple si form.errors est évalué à True.

If an optional field doesn’t appear in the form’s data, the resulting model instance uses the model field default, if there is one, for that field. This behavior doesn’t apply to fields that use CheckboxInput, CheckboxSelectMultiple, or SelectMultiple (or any custom widget whose value_omitted_from_data() method always returns False) since an unchecked checkbox and unselected <select multiple> don’t appear in the data of an HTML form submission. Use a custom form field or widget if you’re designing an API and want the default fallback behavior for a field that uses one of these widgets.

Changed in Django 1.10.1:

Les anciennes versions ne contenaient pas d’exception pour CheckboxInput, ce qui signifie que les cases à cocher non cochées reçoivent la valeur True si cette valeur est celle du champ de modèle par défaut.

Changed in Django 1.10.2:

La méthode value_omitted_from_data() a été ajoutée.

Cette méthode save() accepte un paramètre nommé facultatif commit, qui accepte les valeurs True ou False. Si vous appelez save() avec commit=False, elle renvoie un objet qui n’aura pas encore été enregistré dans la base de données. Dans ce cas, c’est à vous d’appeler save() sur l’instance de modèle obtenue. C’est utile lorsque vous souhaitez effectuer un traitement particulier sur l’objet avant qu’il soit enregistré, ou si vous voulez utiliser l’une des options d’enregistrement de modèle spécialisées. commit vaut True par défaut.

Un autre effet de bord de l’utilisation de commit=False se voit lorsque le modèle possède une relation plusieurs-à-plusieurs vers un autre modèle. Dans ce cas, Django ne peut pas enregistrer immédiatement les données du formulaire de la relation plusieurs-à-plusieurs. La raison est qu’il n’est pas possible d’enregistrer des données plusieurs-à-plusieurs propres à une instance si celle-ci n’existe pas encore dans la base de données.

Pour contourner ce problème, chaque fois que vous enregistrez un formulaire avec commit=False, Django ajoute une méthode save_m2m() à la sous-classe de votre ModelForm. Après avoir manuellement enregistré l’instance dérivée du formulaire, vous pouvez appeler save_m2m() pour enregistrer les données de formulaire de type plusieurs-à-plusieurs. Par exemple :

# Create a form instance with POST data.
>>> f = AuthorForm(request.POST)

# Create, but don't save the new author instance.
>>> new_author = f.save(commit=False)

# Modify the author in some way.
>>> new_author.some_field = 'some_value'

# Save the new instance.
>>> new_author.save()

# Now, save the many-to-many data for the form.
>>> f.save_m2m()

Il n’est utile d’appeler save_m2m() que si vous utilisez save(commit=False). Lorsque vous appelez simplement la méthode save() d’un formulaire, toutes les données, y compris les données plusieurs-à-plusieurs, sont enregistrées sans avoir besoin de faire quoi que ce soit d’autre. Par exemple :

# Create a form instance with POST data.
>>> a = Author()
>>> f = AuthorForm(request.POST, instance=a)

# Create and save the new author instance. There's no need to do anything else.
>>> new_author = f.save()

À part les méthodes save() et save_m2m(), un ModelForm fonctionne exactement de la même manière que tout autre formulaire forms. Par exemple, la méthode is_valid() est utilisée pour vérifier la validité, la méthode is_multipart() est utilisée pour déterminer si un formulaire nécessite un envoi de fichier multipart (et donc que request.FILES doit être transmis au formulaire), etc. Voir Liaison de fichiers téléversés avec un formulaire pour plus d’informations.

Sélection des champs à utiliser

Il est fortement recommandé de définir explicitement tous les champs qui doivent être présents dans le formulaire en utilisant l’attribut fields. Si vous ne le faites pas, cela peut facilement devenir source de problèmes de sécurité lorsqu’un formulaire permet à un utilisateur de définir certains champs de manière non prévue, particulièrement lorsqu’on ajoute de nouveaux champs à un modèle. En fonction de la façon dont le formulaire est affiché, il est même possible que le problème ne soit pas visible sur la page Web.

Une autre approche est d’inclure automatiquement tous les champs ou d’en exclure certains. Mais à la base, cette approche est notoirement moins sécurisée et a déjà abouti à des failles sévères sur des sites Web célèbres (par ex. GitHub).

Il existe toutefois deux raccourcis disponibles pour les cas où vous pouvez garantir que ces questions de sécurité ne s’appliquent pas à votre situation :

  1. Définir l’attribut fields à la valeur spéciale '__all__' pour indiquer que tous les champs du modèle doivent être utilisés. Par exemple :

    from django.forms import ModelForm
    
    class AuthorForm(ModelForm):
        class Meta:
            model = Author
            fields = '__all__'
    
  2. Définir l’attribut exclude de la classe interne Meta du ModelForm à une liste de champs à exclure du formulaire.

    Par exemple :

    class PartialAuthorForm(ModelForm):
        class Meta:
            model = Author
            exclude = ['title']
    

    Comme le modèle Author possède les trois champs name, title et birth_date, nous nous retrouvons avec les champs name et birth_date affichés dans le formulaire.

Si l’une de ces techniques est employée, l’ordre d’apparition des champs dans le formulaire respecte l’ordre de définition des champs du modèle, avec les instances ManyToManyField apparaissant en dernier.

De plus, Django applique la règle suivante : si vous définissez editable=False au niveau du champ de modèle, tout formulaire créé à partir du modèle via ModelForm ne contiendra pas ce champ.

Note

Tout champ non inclus dans un formulaire par la logique ci-dessus ne sera pas touché par la méthode save() du formulaire. De même, si vous ajoutez manuellement dans un formulaire un champ initialement exclu, ce champ ne sera pas initialisé avec les données de l’instance de modèle.

Django empêche toute tentative d’enregistrer un modèle incomplet. Si le modèle n’autorise pas les champs manquants à être vides et que ces champs n’ont pas de valeur par défaut, tout tentative d’appeler save() sur un ModelForm avec des champs manquants échouera. Pour éviter ce problème, vous devez créer une instance de modèle comportant des valeurs initiales pour les champs obligatoires manquants :

author = Author(title='Mr')
form = PartialAuthorForm(request.POST, instance=author)
form.save()

Une variante est d’utiliser save(commit=False) et de définir manuellement les champs obligatoires supplémentaires :

form = PartialAuthorForm(request.POST)
author = form.save(commit=False)
author.title = 'Mr'
author.save()

Consultez la section sur l’enregistrement des formulaires pour plus de détails sur l’utilisation de save(commit=False).

Surcharge des champs par défaut

Les types de champs par défaut, tels que décrits dans le tableau ci-dessus Types de champs, sont des choix raisonnables. Si votre modèle contient un champ DateField, il est probable que vous vouliez représenter ce champ dans un formulaire par un champ de formulaire DateField. Mais ModelForm apporte la souplesse de pouvoir modifier le champ de formulaire pour un modèle défini.

Pour indiquer un composant personnalisé pour un champ, utilisez l’attribut widgets de la classe Meta interne. Ce doit être un dictionnaire faisant correspondre les noms de champs à des classes ou des instances de composants.

Par exemple, si vous voulez que le champ CharField de l’attribut name du modèle Author soit représenté par un composant <textarea> au lieu du composant <input type="text"> par défaut, vous pouvez surcharger le composant du champ :

from django.forms import ModelForm, Textarea
from myapp.models import Author

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date')
        widgets = {
            'name': Textarea(attrs={'cols': 80, 'rows': 20}),
        }

Le dictionnaire widgets accepte aussi bien des instances de composants (par ex. Textarea(...)) que des classes (par ex. Textarea).

De la même façon, vous pouvez indiquer les attributs labels, help_texts et error_messages de la classe interne Meta si vous avez besoin de personnaliser davantage un champ.

Par exemple, si vous souhaitez personnaliser la formulation de tous les textes visibles pour l’utilisateur concernant le champ name:

from django.utils.translation import ugettext_lazy as _

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date')
        labels = {
            'name': _('Writer'),
        }
        help_texts = {
            'name': _('Some useful help text.'),
        }
        error_messages = {
            'name': {
                'max_length': _("This writer's name is too long."),
            },
        }

Vous pouvez aussi indiquer field_classes pour personnaliser le type des champs créés par le formulaire.

Par exemple, si vous souhaitez utiliser MySlugFormField pour le champ slug, vous pourriez procéder ainsi :

from django.forms import ModelForm
from myapp.models import Article

class ArticleForm(ModelForm):
    class Meta:
        model = Article
        fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
        field_classes = {
            'slug': MySlugFormField,
        }

Finalement, si vous souhaitez contrôler complètement un champ, y compris son type, ses validateurs, son caractère obligatoire, etc., vous pouvez le faire en définissant les champs de manière déclarative comme on le ferait pour un formulaire Form normal.

Si vous souhaitez indiquer les validateurs d’un champ, vous pouvez le faire en définissant le champ de manière déclarative et en indiquant le paramètre validators:

from django.forms import ModelForm, CharField
from myapp.models import Article

class ArticleForm(ModelForm):
    slug = CharField(validators=[validate_slug])

    class Meta:
        model = Article
        fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']

Note

Lorsque vous créez une instance de formulaire explicitement comme ceci, il est important de comprendre comment les formulaires de type ModelForm et Form sont liés.

ModelForm est comme un formulaire Form normal qui peut générer automatiquement certains champs. Les champs automatiquement générés dépendent du contenu de la classe Meta et des champs éventuellement définis de manière déclarative. Au fond, ModelForm va uniquement générer les champs qui sont absents du formulaire, ou en d’autres termes, les champs qui n’ont pas été définis déclarativement.

Les champs définis déclarativement sont laissés tels quels, ce qui fait que toute personnalisation effectuée au niveau des attributs de Meta, tels que widgets, labels, help_texts ou error_messages est ignorée ; ces derniers ne s’appliquent qu’aux champs automatiquement générés.

De même, les champs définis déclarativement ne dérivent pas leurs attributs comme max_length ou required du modèle correspondant. Si vous voulez conserver le comportement défini au niveau du modèle, vous devez définir les paramètres adéquats explicitement dans la déclaration du champ de formulaire.

Par exemple, si le modèle Article ressemble à ceci :

class Article(models.Model):
    headline = models.CharField(
        max_length=200,
        null=True,
        blank=True,
        help_text='Use puns liberally',
    )
    content = models.TextField()

et que vous vouliez effectuer une validation particulière pour headline tout en conservant les valeurs blank et help_text d’origine, voici comment vous pourriez définir ArticleForm:

class ArticleForm(ModelForm):
    headline = MyFormField(
        max_length=200,
        required=False,
        help_text='Use puns liberally',
    )

    class Meta:
        model = Article
        fields = ['headline', 'content']

Vous devez vous assurer que le type du champ de formulaire peut être utilisé pour définir le contenu du champ de modèle correspondant. S’ils ne sont pas compatibles, vous obtiendrez une erreur ValueError car aucune conversion implicite n’est effectuée.

Consultez la documentation des champs de formulaire pour plus d’informations sur les champs et leurs paramètres.

Activation de la régionalisation des champs

Par défaut, les champs d’un ModelForm ne régionalisent pas leurs données. Pour activer cette régionalisation, vous pouvez utiliser l’attribut localized_fields de la classe Meta.

>>> from django.forms import ModelForm
>>> from myapp.models import Author
>>> class AuthorForm(ModelForm):
...     class Meta:
...         model = Author
...         localized_fields = ('birth_date',)

Si localized_fields est défini à la valeur spéciale '__all__', les valeurs de tous les champs seront régionalisées.

Héritage de formulaire

Comme pour les formulaires de base, il est possible d’étendre et de réutiliser les ModelForms par l’héritage. C’est utile lorsque vous devez déclarer des champs ou des méthodes supplémentaires dans une sous-classe pour les utiliser dans des formulaires dérivés de modèles. Par exemple, en utilisant la classe ArticleForm présentée précédemment :

>>> class EnhancedArticleForm(ArticleForm):
...     def clean_pub_date(self):
...         ...

Cela crée un formulaire qui se comporte comme ArticleForm, à l’exception de certaines validations et nettoyages supplémentaires pour le champ pub_date.

Il est aussi possible d’hériter de la classe Meta interne du parent si vous avez besoin de modifier les listes Meta.fields ou Meta.exclude:

>>> class RestrictedArticleForm(EnhancedArticleForm):
...     class Meta(ArticleForm.Meta):
...         exclude = ('body',)

Nous conservons ici la méthode supplémentaire de EnhancedArticleForm et enlevons un champ de la classe ArticleForm.Meta originale.

Il faut cependant relever certains points.

  • Les règles Python habituelles de la résolution de noms s’appliquent. Si vous avez plusieurs classes de base déclarant une classe interne Meta, seule la première sera considérée. C’est -à-dire la classe Meta de la sous-classe, s’il y en a une, sinon celle du premier parent, etc.

  • Il est possible d’hériter à la fois de Form et de ModelForm. Cependant, il faut s’assurer que ModelForm apparaisse en premier dans la chaîne MRO. Ceci parce que ces classes se basent sur des métaclasses différentes et qu’une classe ne peut posséder qu’une seule métaclasse.

  • Il est possible d’enlever de manière déclarative un champ Field hérité d’une classe parente en définissant son nom à None dans la sous-classe.

    Cette technique n’est utilisable que pour enlever des champs qui ont eux-mêmes été définis de manière déclarative dans la classe parente ; cela n’empêchera pas la métaclasse de ModelForm de générer un champ par défaut. Pour enlever des champs par défaut, voir Sélection des champs à utiliser.

Indication de valeurs initiales

Comme avec les formulaires normaux, il est possible de spécifier les données initiales pour les formulaires en spécifiant un paramètre initial lors de l’instanciation du formulaire. Les valeurs initiales prévues de cette façon remplaceront la valeur initiale définie pour le champ et celle définie pour le modèle lié. Par exemple

>>> article = Article.objects.get(pk=1)
>>> article.headline
'My headline'
>>> form = ArticleForm(initial={'headline': 'Initial headline'}, instance=article)
>>> form['headline'].value()
'Initial headline'

Fonction de fabrique de ModelForm

Vous pouvez créer des formulaires à partir d’un modèle donné en utilisant la fonction autonome modelform_factory() au lieu de passer par une définition de classe. Cela peut être plus pratique si vous n’avez pas à effectuer trop d’adaptations :

>>> from django.forms import modelform_factory
>>> from myapp.models import Book
>>> BookForm = modelform_factory(Book, fields=("author", "title"))

Cela peut également être utilisé pour faire des modifications simples à des formulaires existants, par exemple pour indiquer les composants à utiliser pour un certain champ :

>>> from django.forms import Textarea
>>> Form = modelform_factory(Book, form=BookForm,
...                          widgets={"title": Textarea()})

Les champs à inclure peuvent être spécifiés en utilisant les paramètres nommés fields et exclude, ou les attributs correspondants de la classe interne Meta du ModelForm. Veuillez consulter la documentation Sélection des champs à utiliser des ModelForm.

… ou pour activer la régionalisation de certains champs :

>>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",))

Formulaires groupés de modèles

class models.BaseModelFormSet

Comme pour les formulaires groupés normaux, Django fournit quelques classes de formulaires groupés améliorées qui facilitent le travail avec les modèles Django. Réutilisons le modèle Author défini plus haut :

>>> from django.forms import modelformset_factory
>>> from myapp.models import Author
>>> AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))

L’utilisation de fields indique aux formulaires groupés les champs qu’ils doivent utiliser. Il est aussi possible d’utiliser une approche par exclusion, indiquant quels sont les champs à exclure :

>>> AuthorFormSet = modelformset_factory(Author, exclude=('birth_date',))

Cela crée un jeu de formulaires groupés qui sont capables de gérer les données associées au modèle Author. Le fonctionnement est le même que pour des formulaires groupés normaux :

>>> 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" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_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>---------</option>
<option value="MR">Mr.</option>
<option value="MRS">Mrs.</option>
<option value="MS">Ms.</option>
</select><input type="hidden" name="form-0-id" id="id_form-0-id" /></td></tr>

Note

modelformset_factory() utilise formset_factory() pour générer les formulaires groupés. Cela signifie que les formulaires groupés de modèle ne sont qu’une extension des formulaires groupés de base, mais qui savent comment interagir avec un modèle particulier.

Modification du jeu de requête

Par défaut, lorsque vous créez des formulaires groupés à partir d’un modèle, ceux-ci utilisent un jeu de requête qui contient tous les objets du modèle (par ex. Author.objects.all()). Vous pouvez surcharger ce comportement en utilisant le paramètre queryset:

>>> formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))

Comme variante, vous pouvez créer une sous-classe définissant self.queryset dans sa méthode __init__:

from django.forms import BaseModelFormSet
from myapp.models import Author

class BaseAuthorFormSet(BaseModelFormSet):
    def __init__(self, *args, **kwargs):
        super(BaseAuthorFormSet, self).__init__(*args, **kwargs)
        self.queryset = Author.objects.filter(name__startswith='O')

Puis, passez votre classe BaseAuthorFormSet à la fonction de fabrique :

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=('name', 'title'), formset=BaseAuthorFormSet)

Si vous souhaitez renvoyer des formulaires groupés qui ne contiennent aucune instance préexistante du modèle, vous pouvez indiquer un objet QuerySet vide :

>>> AuthorFormSet(queryset=Author.objects.none())

Modification du formulaire

Par défaut, lorsque vous utilisez modelformset_factory, un formulaire de modèle est créé par modelform_factory(). Il peut souvent être utile d’indiquer un formulaire de modèle personnalisé. Par exemple, vous pouvez créer un formulaire de modèle personnalisé comportant une validation personnalisée :

class AuthorForm(forms.ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title')

    def clean_name(self):
        # custom validation for the name field
        ...

Puis, indiquez ce formulaire de modèle à la fonction de fabrication :

AuthorFormSet = modelformset_factory(Author, form=AuthorForm)

Il n’est pas toujours nécessaire de définir un formulaire de modèle personnalisé. La fonction modelformset_factory accepte plusieurs paramètres qui sont eux-mêmes transmis à modelform_factory et qui sont documentés plus bas.

Indication des composants de formulaire à utiliser avec widgets

En exploitant le paramètre widgets, vous pouvez indiquer un dictionnaire de valeurs afin de personnaliser la classe de composant d’un champ particulier du ModelForm. C’est le même principe de fonctionnement que le dictionnaire widgets de la classe Meta interne d’un ModelForm:

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=('name', 'title'),
...     widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20})})

Activation de la régionalisation des champs avec localized_fields

À l’aide du paramètre localized_fields, vous pouvez activer la régionalisation des champs du formulaire.

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=('name', 'title', 'birth_date'),
...     localized_fields=('birth_date',))

Si localized_fields est défini à la valeur spéciale '__all__', les valeurs de tous les champs seront régionalisées.

Indication de valeurs initiales

Comme pour les formulaires groupés normaux, il est possible de fournir des données initiales aux formulaires groupés en indiquant le paramètre initial au moment de la création de l’instance de classe de formulaires groupés de modèles renvoyée par modelformset_factory(). Cependant, avec les formulaires groupés de modèles, les valeurs initiales ne s’appliquent qu’aux formulaires supplémentaires, ceux qui ne sont pas liés à des instances d’objets existants. Si les formulaires avec données initiales ne sont pas modifiés par l’utilisateur, ils ne seront ni validés, ni enregistrés.

Enregistrement des objets des formulaires groupés

Comme pour un ModelForm, vous pouvez enregistrer les données sous forme d’objet de modèle. Cela se fait par la méthode save() des formulaires groupés :

# Create a formset instance with POST data.
>>> formset = AuthorFormSet(request.POST)

# Assuming all is valid, save the data.
>>> instances = formset.save()

La méthode save() renvoie les instances qui ont été enregistrées dans la base de données. Si les données d’une instance précise n’ont pas été modifiées dans les données du formulaire, l’instance ne sera pas enregistrée dans la base de données et ne sera pas non plus incluse dans la valeur renvoyée (instances, dans l’exemple ci-dessus).

Lorsque des champs sont absents du formulaire (par exemple en raison de leur exclusion), ces champs ne sont pas modifiés par la méthode save(). Vous pouvez trouver plus d’informations sur cette restriction, qui est aussi valable pour les formulaires ModelForm normaux, dans Sélection des champs à utiliser.

Passez commit=False pour recevoir les instances de modèles non enregistrées :

# don't save to the database
>>> instances = formset.save(commit=False)
>>> for instance in instances:
...     # do something with instance
...     instance.save()

Cela vous permet de définir des données complémentaires dans les instances avant de les enregistrer dans la base de données. Si vos formulaires groupés contiennent un champ ManyToManyField, vous devrez également appeler formset.save_m2m() pour vous assurer que les liens plusieurs-à-plusieurs soient correctement enregistrés.

Après l’appel à save(), les formulaires groupés du modèle posséderont trois nouveaux attributs contenant les modifications des formulaires :

models.BaseModelFormSet.changed_objects
models.BaseModelFormSet.deleted_objects
models.BaseModelFormSet.new_objects

Restriction du nombre d’objets modifiables

Comme pour les formulaires groupés normaux, vous pouvez utiliser les paramètres max_num et extra de modelformset_factory() pour restreindre le nombre de formulaires supplémentaires affichés.

max_num n’empêche pas les objets existants d’être affichés :

>>> Author.objects.order_by('name')
<QuerySet [<Author: Charles Baudelaire>, <Author: Paul Verlaine>, <Author: Walt Whitman>]>

>>> AuthorFormSet = modelformset_factory(Author, fields=('name',), max_num=1)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> [x.name for x in formset.get_queryset()]
['Charles Baudelaire', 'Paul Verlaine', 'Walt Whitman']

De plus, extra=0 n’empêche pas la création de nouvelles instances de modèles car il est possible d”ajouter des formulaires supplémentaires par JavaScript ou simplement d’envoyer des données POST supplémentaires. Les jeux de formulaires ne fournissent pas encore la fonctionnalité d’une vue uniquement en mode édition qui empêcherait la création de nouvelles instances.

Si la valeur de max_num est plus grande que le nombre d’objets liés existants, un maximum de extra formulaires vierges supplémentaires seront ajoutés aux formulaires groupés, aussi longtemps que le nombre total de formulaires n’excède pas max_num:

>>> AuthorFormSet = modelformset_factory(Author, fields=('name',), max_num=4, extra=2)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> for form in formset:
...     print(form.as_table())
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-0-id" value="1" id="id_form-0-id" /></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><input id="id_form-1-name" type="text" name="form-1-name" value="Paul Verlaine" maxlength="100" /><input type="hidden" name="form-1-id" value="3" id="id_form-1-id" /></td></tr>
<tr><th><label for="id_form-2-name">Name:</label></th><td><input id="id_form-2-name" type="text" name="form-2-name" value="Walt Whitman" maxlength="100" /><input type="hidden" name="form-2-id" value="2" id="id_form-2-id" /></td></tr>
<tr><th><label for="id_form-3-name">Name:</label></th><td><input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></td></tr>

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.

Utilisation de formulaires groupés de modèle dans une vue

Les formulaires groupés de modèle sont très semblables aux formulaires groupés normaux. Admettons que nous voulions afficher des formulaires groupés pour modifier les instances du modèle Author:

from django.forms import modelformset_factory
from django.shortcuts import render
from myapp.models import Author

def manage_authors(request):
    AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))
    if request.method == 'POST':
        formset = AuthorFormSet(request.POST, request.FILES)
        if formset.is_valid():
            formset.save()
            # do something.
    else:
        formset = AuthorFormSet()
    return render(request, 'manage_authors.html', {'formset': formset})

Comme vous pouvez le voir, la logique de la vue de formulaires groupés de modèle n’est pas fondamentalement différente de celle de formulaires groupés normaux. La seule différence est que nous appelons formset.save() pour enregistrer les données dans la base de données (comme cela a été décrit ci-dessus dans Enregistrement des objets des formulaires groupés).

Surcharge de clean() d’un ModelFormSet

Tout comme pour ModelForms, la méthode clean() de ModelFormSet valide par défaut qu’aucun des éléments dans les formulaires groupés ne viole les contraintes d’unicité de votre modèle (que ce soit unique, unique_together ou unique_for_date|month|year). Si vous souhaitez surcharger la méthode clean() d’un ModelFormSet et conserver cette validation, vous devez appeler la méthode clean de la classe parente :

from django.forms import BaseModelFormSet

class MyModelFormSet(BaseModelFormSet):
    def clean(self):
        super(MyModelFormSet, self).clean()
        # example custom validation across forms in the formset
        for form in self.forms:
            # your custom formset validation
            ...

Notez également qu’une fois que vous avez atteint cette étape, les instances de modèles individuelles pour chaque formulaire ont déjà été créées. Il ne suffit donc pas de modifier une valeur dans form.cleaned_data pour changer la valeur enregistrée. Si vous souhaitez modifier une valeur dans ModelFormSet.clean(), vous devez modifier form.instance:

from django.forms import BaseModelFormSet

class MyModelFormSet(BaseModelFormSet):
    def clean(self):
        super(MyModelFormSet, self).clean()

        for form in self.forms:
            name = form.cleaned_data['name'].upper()
            form.cleaned_data['name'] = name
            # update the instance value.
            form.instance.name = name

Utilisation d’un jeu de requête personnalisé

Comme indiqué précédemment, il est possible de surcharger le jeu de requête utilisé par défaut par les formulaires groupés de modèle :

from django.forms import modelformset_factory
from django.shortcuts import render
from myapp.models import Author

def manage_authors(request):
    AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))
    if request.method == "POST":
        formset = AuthorFormSet(
            request.POST, request.FILES,
            queryset=Author.objects.filter(name__startswith='O'),
        )
        if formset.is_valid():
            formset.save()
            # Do something.
    else:
        formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))
    return render(request, 'manage_authors.html', {'formset': formset})

Notez que nous transmettons le paramètre queryset à la fois aux requêtes POST et GET dans cet exemple.

Utilisation des formulaires groupés dans un gabarit

Il y a trois façons d’afficher des formulaires groupés dans un gabarit Django.

Premièrement, vous pouvez déléguer aux formulaires groupés l’essentiel du travail :

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

Deuxièmement, vous pouvez afficher les formulaires groupés manuellement, mais laisser le soin à chaque formulaire de s’afficher lui-même :

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

Lorsque vous vous occupez vous-même d’afficher les différents formulaires, prenez soin d’inclure également le formulaire de gestion comme le montre l’exemple ci-dessus. Voir la documentation du formulaire de gestion.

Troisièmement, vous pouvez afficher manuellement chaque champ :

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

Si vous optez pour cette troisième méthode et que vous ne bouclez pas sur les champs avec un {% for %}, vous devez aussi inclure le champ de clé primaire. Par exemple, si vous affichez les champs name et age d’un modèle :

<form method="post" action="">
    {{ formset.management_form }}
    {% for form in formset %}
        {{ form.id }}
        <ul>
            <li>{{ form.name }}</li>
            <li>{{ form.age }}</li>
        </ul>
    {% endfor %}
</form>

Voyez comment nous devons explicitement afficher {{ form.id }}. Ceci pour s’assurer du bon fonctionnement des formulaires groupés de modèle, dans le cas de l’envoi POST (cet exemple suppose que la clé primaire s’appelle id. Si vous avez explicitement nommé la clé primaire su modèle autrement que id, prenez soin de l’afficher).

Sous-formulaires groupés

class models.BaseInlineFormSet

Les sous-formulaires groupés sont une petite couche d’abstraction au-dessus des formulaires groupés de modèle. Ils simplifient la situation où l’on manipule des objets liés au travers d’une clé étrangère. Supposons que l’on dispose de ces deux modèles :

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

Si vous voulez créer des formulaires groupés permettant de modifier les livres appartenant à un auteur particulier, vous pourriez faire ainsi :

>>> from django.forms import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book, fields=('title',))
>>> author = Author.objects.get(name='Mike Royko')
>>> formset = BookFormSet(instance=author)

Note

inlineformset_factory() utilise modelformset_factory() et définit can_delete=True.

Surcharge des méthodes d’un InlineFormSet

Lorsque vous surchargez des méthodes de sous-formulaires groupés (InlineFormSet), il est préférable d’hériter de BaseInlineFormSet plutôt que de BaseModelFormSet.

Par exemple, si vous souhaitez surcharger clean():

from django.forms import BaseInlineFormSet

class CustomInlineFormSet(BaseInlineFormSet):
    def clean(self):
        super(CustomInlineFormSet, self).clean()
        # example custom validation across forms in the formset
        for form in self.forms:
            # your custom formset validation
            ...

Voir aussi Surcharge de clean() d’un ModelFormSet.

Puis, au moment de créer les sous-formulaires groupés, passez le paramètre facultatif formset:

>>> from django.forms import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book, fields=('title',),
...     formset=CustomInlineFormSet)
>>> author = Author.objects.get(name='Mike Royko')
>>> formset = BookFormSet(instance=author)

Plus d’une clé étrangère vers le même modèle

Si votre modèle contient plus d’une clé étrangère vers le même modèle, vous devrez résoudre l’ambiguïté manuellement en utilisant fk_name. Par exemple, considérez le modèle suivant :

class Friendship(models.Model):
    from_friend = models.ForeignKey(
        Friend,
        on_delete=models.CASCADE,
        related_name='from_friends',
    )
    to_friend = models.ForeignKey(
        Friend,
        on_delete=models.CASCADE,
        related_name='friends',
    )
    length_in_months = models.IntegerField()

Pour résoudre cela, vous pouvez définir fk_name dans inlineformset_factory():

>>> FriendshipFormSet = inlineformset_factory(Friend, Friendship, fk_name='from_friend',
...     fields=('to_friend', 'length_in_months'))

Utilisation de sous-formulaires groupés dans une vue

Il peut être nécessaire de définir une vue permettant à un utilisateur de modifier les objets liés d’un modèle. Voici comment on peut le faire :

def manage_books(request, author_id):
    author = Author.objects.get(pk=author_id)
    BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
    if request.method == "POST":
        formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
        if formset.is_valid():
            formset.save()
            # Do something. Should generally end with a redirect. For example:
            return HttpResponseRedirect(author.get_absolute_url())
    else:
        formset = BookInlineFormSet(instance=author)
    return render(request, 'manage_books.html', {'formset': formset})

Notez comment nous définissons le paramètre instance aussi bien dans le cas POST que le cas GET.

Indication des composants à utiliser dans le sous-formulaire

inlineformset_factory utilise modelformset_factory et transmet la plupart de ses paramètres à modelformset_factory. Cela signifie que vous pouvez utiliser le paramètre widgets tout comme vous le feriez pour modelformset_factory. Voir Indication des composants de formulaire à utiliser avec widgets ci-dessus.

Back to Top