La validation de formulaires et de champs¶
La validation de formulaire intervient lorsque les donnĂ©es sont nettoyĂ©es. Si vous souhaitez personnaliser ce processus, il y a plusieurs points dâentrĂ©e oĂč des changements peuvent se faire, chacun visant un objectif diffĂ©rent. Trois types de mĂ©thodes de nettoyage sont exĂ©cutĂ©s durant le traitement dâun formulaire. Elles sont normalement exĂ©cutĂ©es lorsque la mĂ©thode is_valid()
dâun formulaire est appelĂ©e. Il y a dâautres opĂ©rations qui peuvent aussi provoquer le nettoyage et la validation (lâaccĂšs Ă lâattribut errors
ou lâappel direct Ă full_clean()
), mais en principe, cela ne fait pas partie du processus normal.
En gĂ©nĂ©ral, toute mĂ©thode de nettoyage peut gĂ©nĂ©rer lâexception ValidationError
si un problÚme survient lors du traitement des valeurs, en passant des informations adéquates au constructeur de ValidationError
. Voir ci-dessous pour les bonnes pratiques de génération de ValidationError
. Si aucune exception ValidationError
nâest produite, la mĂ©thode doit renvoyer la valeur nettoyĂ©e (normalisĂ©e) sous forme dâobjet Python.
La plupart de la validation peut se faire Ă lâaide de validateurs, de simples fonctions utilitaires pouvant facilement ĂȘtre rĂ©utilisĂ©es. Il sâagit de simples fonctions (ou objets exĂ©cutables) acceptant un seul paramĂštre et gĂ©nĂ©rant une exception ValidationError
en cas de saisie non valide. Les validateurs sont exĂ©cutĂ©s aprĂšs lâappel aux mĂ©thodes to_python
et validate
du champ.
La validation dâun formulaire est partagĂ©e en plusieurs Ă©tapes, qui peuvent ĂȘtre personnalisĂ©es ou surchargĂ©es :
La méthode
to_python()
dâun champField
est la premiÚre étape de toute validation. Elle transforme la valeur dans le bon type de données et génÚreValidationError
si ce nâest pas possible. Cette mĂ©thode accepte la valeur brute provenant du composant et renvoie la valeur convertie. Par exemple, un champFloatField
transforme la donnée en un objet Pythonfloat
ou génÚre une exceptionValidationError
.La méthode
validate()
dâun champField
se charge de la validation spécifique du champ qui ne convient pas pour un validateur. Elle accepte une valeur ayant déjà été convertie dans le bon type et génÚre une exceptionValidationError
en cas dâerreur. Cette mĂ©thode ne renvoie rien et ne doit pas modifier la valeur. Elle peut ĂȘtre surchargĂ©e pour gĂ©rer de la logique de validation que vous ne pouvez ou ne voulez pas placer dans un validateur.La mĂ©thode
run_validators()
dâun champ lance tous les validateurs du champ et rassemble toutes les erreurs dans une seule exceptionValidationError
. Il nâest en principe pas utile de surcharger cette mĂ©thode.La mĂ©thode
clean()
dâune sous-classe de champ est responsable de lâexĂ©cution deto_python()
,validate()
etrun_validators()
dans le bon ordre et de la propagation des erreurs. Si Ă tout moment lâune de ces mĂ©thodes gĂ©nĂšre une exceptionValidationError
, la validation sâarrĂȘte et cette erreur est propagĂ©e. Cette mĂ©thode renvoie les donnĂ©es nettoyĂ©es qui sont ensuite insĂ©rĂ©es dans le dictionnairecleaned_data
du formulaire.La méthode
clean_<nom_du_champ>()
est appelĂ©e pour une sous-classe de formulaire â oĂč<nom_du_champ>
est remplacĂ© par le nom de lâattribut de champ de formulaire. Cette mĂ©thode sâoccupe de tout nettoyage spĂ©cifique Ă cet attribut, sans considĂ©ration du type de champ concernĂ©. Cette mĂ©thode ne reçoit aucun paramĂštre. Vous devez chercher vous-mĂȘme la valeur du champ dansself.cleaned_data
et vous rappeler quâil sâagira Ă ce moment dâun objet Python, pas de la chaĂźne initialement soumise avec le formulaire (la valeur figure danscleaned_data
parce que la méthode généraleclean()
du champ aura dĂ©jĂ nettoyĂ© la valeur une premiĂšre fois).Par exemple, si vous souhaitiez valider que le contenu dâun champ
CharField
nomméserialnumber
est unique,clean_serialnumber()
serait le bon endroit pour le faire. Vous nâavez pas besoin dâun champ spĂ©cifique (ce nâest quâunCharField
), mais vous avez besoin dâune sĂ©quence de validation propre au champ de formulaire et, si possible, de nettoyer/normaliser les donnĂ©es.La valeur renvoyĂ©e par cette mĂ©thode remplace la valeur existante dans
cleaned_data
, il doit donc sâagir soit de la valeur decleaned_data
(mĂȘme si cette mĂ©thode ne lâa pas modifiĂ©e), soit dâune nouvelle valeur propre.La mĂ©thode
clean()
de la sous-classe de formulaire peut effectuer toute validation nĂ©cessitant dâaccĂ©der Ă plusieurs champs de formulaire. Câest ici que vous pouvez placer des contrĂŽles du genre : si le champA
est renseigné , le champB
doit contenir une adresse de messagerie valide. Cette méthode peut renvoyer un dictionnaire complÚtement différent si elle le souhaite, et ce résultat sera utilisé comme contenu decleaned_data
.Comme les mĂ©thodes de validation des champs ont Ă©tĂ© exĂ©cutĂ©es au moment oĂč
clean()
est appelĂ©e, vous avez aussi accĂšs Ă lâattributerrors
du formulaire qui contient toutes les erreurs générées lors du nettoyage individuel des champs.Notez que toute erreur générée par votre version de
Form.clean()
ne sera associée à aucun champ particulier. Elles sont attribuées à un « champ » spécial nommé__all__
auquel il est possible dâaccĂ©der par la mĂ©thodenon_field_errors()
en cas de besoin. Si vous souhaitez lier une erreur à un champ spécifique du formulaire, vous devrez appeleradd_error()
.Notez Ă©galement quâil faut tenir compte de considĂ©rations particuliĂšres lors de la surcharge de la mĂ©thode
clean()
dâune sous-classe deModelForm
(voir la documentation de ModelForm pour plus dâinformations).
Ces mĂ©thodes sont exĂ©cutĂ©es dans lâordre metnionnĂ© ci-dessus, un champ aprĂšs lâautre. Câest-Ă -dire que pour chaque champ du formulaire (dans lâordre oĂč ils ont Ă©tĂ© dĂ©clarĂ©s dans la dĂ©finition du formulaire), la mĂ©thode Field.clean()
(ou sa version surchargée) est exécutée, puis clean_<nom_du_champ>()
. Finalement, aprÚs que ces deux méthodes ont été exécutées pour chaque champ, la méthode Form.clean()
ou sa version surchargĂ©e est exĂ©cutĂ©e dans tous les cas, mĂȘme si les mĂ©thodes prĂ©cĂ©dentes ont gĂ©nĂ©rĂ© des erreurs.
Des exemples pour chacune de ces méthodes sont présentés ci-dessous.
Comme mentionné, chacune de ces méthodes peut générer une exception ValidationError
. Pour chaque champ, si la méthode Field.clean()
gĂšnĂšre une erreur ValidationError
, la mĂ©thode de nettoyage spĂ©cifique au champ nâest pas appelĂ©e. Cependant, les mĂ©thodes de nettoyage de tous les autres champs sont tout de mĂȘme exĂ©cutĂ©es.
Génération de ValidationError
¶
Pour une plus grande flexibilitĂ© des messages dâerreur et pour quâil soit plus facile de les surcharger, voici quelques lignes de conduite :
Fournissez un
code
dâerreur descriptif au constructeur :# Good ValidationError(_('Invalid value'), code='invalid') # Bad ValidationError(_('Invalid value'))
Ne fusionnez pas les variables dans les messages ; utilisez des substituants ainsi que le paramĂštre
params
du constructeur :# Good ValidationError( _('Invalid value: %(value)s'), params={'value': '42'}, ) # Bad ValidationError(_('Invalid value: %s') % value)
PrĂ©fĂ©rez la substitution par clĂ© de dictionnaire plutĂŽt que le formatage positionnel. Cela permet de placer les variables dans nâimporte quel ordre ou mĂȘme de les omettre entiĂšrement lors de la réécriture dâun message :
# Good ValidationError( _('Invalid value: %(value)s'), params={'value': '42'}, ) # Bad ValidationError( _('Invalid value: %s'), params=('42',), )
Englobez le message dans un appel
gettext
pour activer sa traduction :# Good ValidationError(_('Invalid value')) # Bad ValidationError('Invalid value')
Pour résumer le tout :
raise ValidationError(
_('Invalid value: %(value)s'),
code='invalid',
params={'value': '42'},
)
Le respect de ces lignes de conduite est particuliÚrement utile si vous écrivez des formulaires, des champs de formulaire ou des champs de modÚle réutilisables.
MĂȘme si ce nâest pas recommandĂ©, si vous vous trouvez Ă la fin de la chaĂźne de validation (par ex. la mĂ©thode clean()
de votre formulaire) et que vous savez que vous nâaurez jamais besoin de surcharger le message dâerreur, vous pouvez toujours opter pour la version plus directe :
ValidationError(_('Invalid value: %s') % value)
Les méthodes Form.errors.as_data()
et Form.errors.as_json()
bĂ©nĂ©ficient grandement dâobjets ValidationError
complÚtement renseignés (avec un nom code
et un dictionnaire params
).
Génération de plusieurs erreurs¶
Si vous dĂ©tectez plusieurs erreurs durant une mĂ©thode de nettoyage et que vous souhaitiez toutes les signaler Ă celui qui a soumis le formulaire, il est possible de transmettre une liste dâerreurs au constructeur de ValidationError
.
Comme mentionnĂ© plus haut, il est recommandĂ© de passer une liste dâinstances ValidationError
avec les paramĂštres code
et params
, mais une liste de chaĂźnes fera aussi lâaffaire :
# Good
raise ValidationError([
ValidationError(_('Error 1'), code='error1'),
ValidationError(_('Error 2'), code='error2'),
])
# Bad
raise ValidationError([
_('Error 1'),
_('Error 2'),
])
Utilisation de la validation en pratique¶
Les sections précédentes ont expliqué comment fonctionne la validation de maniÚre générale pour les formulaires. Comme il est parfois plus simple de remettre les choses à leur place en examinant du code en contexte, voici une série de petits exemples qui font usage de chacune des fonctionnalités présentées précédemment.
Utilisation des validateurs¶
Les champs de formulaire (et de modÚle) de Django gÚrent des fonctions et classes utilitaires simples connus sous le nom de validateurs. Un validateur est un simple objet exécutable acceptant une valeur et ne renvoyant rien du tout si la valeur est valide ou générant une exception ValidationError
si elle ne lâest pas. Ces validateurs peuvent ĂȘtre transmis Ă un constructeur de champ par lâintermĂ©diaire du paramĂštre validators
du champ ou définis dans une classe Field
par leur attribut default_validators
.
Des validateurs simples peuvent ĂȘtre employĂ©s pour valider des valeurs dâun champ ; examinons par exemple le champ SlugField
de Django :
from django.core import validators
from django.forms import CharField
class SlugField(CharField):
default_validators = [validators.validate_slug]
Comme vous pouvez le voir, SlugField
nâest quâun CharField
dotĂ© dâun validateur particulier validant que le texte soumis obĂ©it Ă certaines rĂšgles textuelles. Cela peut aussi se faire lors de la dĂ©finition du champ, ainsi :
slug = forms.SlugField()
est équivalent à :
slug = forms.CharField(validators=[validators.validate_slug])
Des cas courants comme la validation dâune adresse Ă©lectronique ou dâune expression rĂ©guliĂšre peuvent ĂȘtre traitĂ©s avec des classes de validation existantes de Django. Par exemple, validators.validate_slug
est une instance de RegexValidator
construite avec son premier paramÚtre équivalent au motif ^[-a-zA-Z0-9_]+$
. Consultez la section sur lâĂ©criture de validateurs pour voir une liste de ce qui est dĂ©jĂ disponible et pour des exemples de la façon dâĂ©crire un validateur.
Nettoyage par défaut des champs de formulaire¶
CrĂ©ons tout dâabord un champ de formulaire personnalisĂ© qui valide que sa valeur dâentrĂ©e est une chaĂźne contenant des adresses Ă©lectroniques sĂ©parĂ©es par des virgules. La classe complĂšte ressemble Ă ceci :
from django import forms
from django.core.validators import validate_email
class MultiEmailField(forms.Field):
def to_python(self, value):
"""Normalize data to a list of strings."""
# Return an empty list if no input was given.
if not value:
return []
return value.split(',')
def validate(self, value):
"""Check if value consists only of valid emails."""
# Use the parent's handling of required fields, etc.
super().validate(value)
for email in value:
validate_email(email)
Pour chaque formulaire utilisant ce champ, ces mĂ©thodes seront exĂ©cutĂ©es avant que quoi que ce soit puisse ĂȘtre fait avec les donnĂ©es du champ. Il sâagit de nettoyage spĂ©cifique Ă ce type de champ, quelle que soit la maniĂšre dont il sera utilisĂ© par la suite.
Créons un formulaire ContactForm
simple pour montrer comment ce champ peut ĂȘtre utilisĂ© :
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
recipients = MultiEmailField()
cc_myself = forms.BooleanField(required=False)
Utilisez simplement MultiEmailField
comme nâimporte quel autre champ. Lorsque la mĂ©thode is_valid()
est appelée pour le formulaire, la méthode MultiEmailField.clean()
sera aussi exécutée dans le contexte du processus de nettoyage, et celle-ci appellera à son tour les méthodes personnalisées to_python()
et validate()
.
Nettoyage dâun attribut de champ spĂ©cifique¶
En poursuivant lâexemple prĂ©cĂ©dent, supposons que dans notre formulaire ContactForm
, nous aimerions ĂȘtre certain que le champ recipients
contienne toujours lâadresse "fred@example.com"
. Il sâagit de validation spĂ©cifique Ă notre formulaire, ce qui explique que nous ne voulions pas la placer dans la classe gĂ©nĂ©rique MultiEmailField
. Au lieu de cela, nous écrivons une méthode de nettoyage qui agit sur le champ recipients
, comme ceci :
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean_recipients(self):
data = self.cleaned_data['recipients']
if "fred@example.com" not in data:
raise forms.ValidationError("You have forgotten about Fred!")
# Always return a value to use as the new cleaned data, even if
# this method didn't change it.
return data
Nettoyage et validation de champs qui dĂ©pendent lâun de lâautre¶
Supposons que nous ajoutions une autre exigence Ă notre formulaire de contact : si le champ cc_myself
vaut True
, le champ subject
doit contenir le mot "help"
. Nous effectuons de la validation sâappliquant Ă plus dâun champ Ă la fois, câest pourquoi la mĂ©thode clean()
du formulaire est le bon endroit pour cela. Remarquez que nous parlons bien maintenant de la méthode clean()
du formulaire, alors quâauparavant nous avions Ă©crit une mĂ©thode clean()
pour un champ. Il est important de bien faire la diffĂ©rence entre le champ et le formulaire lorsquâil sâagit de la validation de contenu. Les champs sont des points de donnĂ©es uniques, les formulaires sont des ensembles de champs.
Au moment oĂč la mĂ©thode clean()
du formulaire est appelée, toutes les méthodes de nettoyage de chaque champ auront déjà été exécutées (cf. les deux sections précédentes), ce qui fait que self.cleaned_data
sera rempli par toute donnĂ©e ayant respectĂ© la validation jusque lĂ . Il faut donc aussi se rappeler de tenir compte du fait que les champs que vous souhaitez valider pourraient ne pas avoir passĂ© lâĂ©tape prĂ©alable de la vĂ©rification au niveau du champ individuel.
Il y a deux maniĂšres de signaler des erreurs Ă ce stade. La mĂ©thode probablement la plus courante est dâafficher lâerreur au sommet du formulaire. Pour crĂ©er une telle erreur, vous pouvez gĂ©nĂ©rer une exception ValidationError
à partir de la méthode clean()
. Par exemple :
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject:
# Only do something if both fields are valid so far.
if "help" not in subject:
raise forms.ValidationError(
"Did not send for 'help' in the subject despite "
"CC'ing yourself."
)
Dans ce code, si lâerreur de validation est gĂ©nĂ©rĂ©e, le formulaire affichera un message dâerreur au sommet du formulaire (dans le cas normal) dĂ©crivant le problĂšme.
Lâappel Ă super().clean()
dans lâexemple de code garantit que toute logique de validation dans les classes parentes est conservĂ©e. Si votre formulaire hĂ©rite dâun autre qui ne renvoie pas de dictionnaire cleaned_data
dans sa méthode clean()
(câest facultatif), nâattribuez pas le rĂ©sultat de lâappel Ă super()
Ă cleaned_data
et utilisez plutĂŽt self.cleaned_data
:
def clean(self):
super().clean()
cc_myself = self.cleaned_data.get("cc_myself")
...
La seconde approche pour signaler les erreurs de validation pourrait impliquer dâattribuer le message dâerreur Ă lâun des champs. Dans ce cas, attribuons un message dâerreur Ă la fois aux deux lignes « subject » et « cc_myself » dans lâaffichage du formulaire. Soyez prudent si vous faites cela en pratique, car cela pourrait amener de la confusion dans la prĂ©sentation du formulaire. Nous montrons ici ce qui est possible, mais nous vous laissons la responsabilitĂ© de constater vous-mĂȘme ce qui est faisable dans votre contexte particulier. Notre nouveau code (remplaçant lâexemple prĂ©cĂ©dent) ressemble Ă ceci :
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
msg = "Must put 'help' in subject when cc'ing yourself."
self.add_error('cc_myself', msg)
self.add_error('subject', msg)
Le second paramĂštre de add_error()
peut ĂȘtre une simple chaĂźne, ou de prĂ©fĂ©rence une instance de ValidationError
. Consultez Génération de ValidationError pour plus de détails. Notez que add_error()
enlĂšve automatiquement le champ de cleaned_data
.