gwift-book/chapters/forms.tex

178 lines
7.9 KiB
TeX
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\chapter{Forms}
Ou comment valider proprement des données entrantes.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/xkcd-327.png}}
\caption{XKCD 327}
\end{figure}
\begin{quote}
Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et que l'ensemble reste cohérent.
Il (le form) n'a pas à savoir que tu as implémenté des closure tables dans un graph dirigé acyclique.
\end{quote}
Quand on parle de \texttt{forms}, on ne parle pas uniquement de formulaires Web.
On pourrait considérer qu'il s'agit de leur objectif principal, mais on peut également voir un peu plus loin: on peut en fait voir les \texttt{forms} comme le point d'entrée pour chaque donnée arrivant dans notre application: il s'agit en quelque sorte d'un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle.
L'exemple le plus simple est un fichier \texttt{.csv} : la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle.
Mauvaise idée.
On peut proposer trois versions d'un même code, de la version simple (lecture du fichier csv et jonglage avec les indices de colonnes), puis une version plus sophistiquée (et plus lisible, à base de \href{https://docs.python.org/3/library/csv.html\#csv.DictReader}{DictReader}), et la version + à base de form.
Les données fournies par un utilisateur \textbf{doivent} \textbf{toujours} être validées avant introduction dans la base de données.
Notre base de données étant accessible ici par l'ORM, la solution consiste à introduire une couche supplémentaire de validation.
Le flux à suivre est le suivant:
\begin{enumerate}
\item Création d'une instance grâce à un dictionnaire
\item Validation des données et des informations reçues
\item Traitement, si la validation a réussi.
\end{enumerate}
Ils jouent également deux rôles importants:
\begin{enumerate}
\item Valider des données, en plus de celles déjà définies au niveau du modèle
\item Contrôler le rendu à appliquer aux champs.
\end{enumerate}
Ils agissent come une glue entre l'utilisateur et la modélisation de vos structures de données.
\section{Flux de validation}
\textbar{} .Validation \textbar{} .is\_valid \textbar{} .clean\_fields ↓
.clean\_fields\_machin
Tout comme pour le modèle (+ ref), l'idée est simplement de définir plusieurs niveaux de validation.
\section{Gestion du changement}
Dans le package \texttt{django.contrib.admin.utils}, on trouve une petite pépite du nom de \texttt{construct\_change\_message}.
Cette fonction permet de construire un message de changement à partir de n'importe que \texttt{form} ou \texttt{formset}.
Elle prend en paramètre une valeur booléenne supplémentaire qui indique s'il s'agit d'un ajout ou pas.
Le résultat retourne une structure qui indique les champs qui ont été modifiés, les champs qui ont été ajoutés ou supprimés:
\begin{minted}{python}
def construct_change_message(form, formsets, add):
[snip]
change_message = []
if add:
change_message.append({"added": {}})
elif form.changed_data:
change_message.append({"changed": {"fields": changed_field_labels}})
if formsets:
with translation_override(None):
for formset in formsets:
for added_object in formset.new_objects:
change_message.append(
{
"added": {
"name": str(added_object._meta.verbose_name),
"object": str(added_object),
}
}
)
for changed_object, changed_fields in formset.changed_objects:
change_message.append(
{
"changed": {
"name": str(changed_object._meta.verbose_name),
"object": str(changed_object),
"fields": _get_changed_field_labels_from_form(
formset.forms[0], changed_fields
),
}
}
)
for deleted_object in formset.deleted_objects:
change_message.append(
{
"deleted": {
"name": str(deleted_object._meta.verbose_name),
"object": str(deleted_object),
}
}
)
return change_message
\end{minted}
\section{Dépendance avec le modèle}
Un \textbf{form} peut hériter d'une autre classe Django.
Pour cela, il suffit de fixer l'attribut \texttt{model} au niveau de la \texttt{class\ Meta} dans la définition.
\begin{minted}{python}
from django import forms
from wish.models import Wishlist
class WishlistCreateForm(forms.ModelForm):
class Meta:
model = Wishlist
fields = ('name', 'description')
\end{minted}
Notre form dépendra automatiquement des champs déjà déclarés dans la classe \texttt{Wishlist}.
Cela suit le principe de \texttt{DRY\ \textless{}dont\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ quune\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ lattribut\ \textasciigrave{}fields},
nous pourrons nous assurer de faire évoluer le formulaire en fonction du
modèle sur lequel il se base.
L'intérêt du form est de créer une isolation par rapport aux données provenant de l'extérieur.
\section{Rendu et affichage}
Le formulaire permet également de contrôler le rendu qui sera appliqué lors de la génération de la page.
Si les champs dépendent du modèle sur lequel se base le formulaire, ces widgets doivent être initialisés dans
l'attribut \texttt{Meta}.
Sinon, ils peuvent l'être directement au niveau du champ.
\subsection{Squelette par défaut}
\subsubsection{as\_p}
\subsubsection{as\_table}
\subsubsection{Vitor}
\subsection{Crispy-forms}
Comme on l'a vu à l'instant, les forms, en Django, c'est le bien.
Cela permet de valider des données reçues en entrée et d'afficher (très) facilement des formulaires à compléter par l'utilisateur.
Par contre, c'est lourd.
Dès qu'on souhaite peaufiner un peu l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir,
modifier les types de contrôleurs, les placer au pixel près, \ldots
Tout ça demande énormément de temps.
Et c'est là qu'intervient \href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}.
Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le \textbf{layout} et la présentation.
(c/c depuis le lien ci-dessous)
Pour chaque champ, crispy-forms va :
\begin{itemize}
\item
utiliser le \texttt{verbose\_name} comme label.
\item
vérifier les paramètres \texttt{blank} et \texttt{null} pour savoir si le champ est obligatoire.
\item
utiliser le type de champ pour définir le type de la balise \texttt{\textless{}input\textgreater{}}.
\item
récupérer les valeurs du paramètre \texttt{choices} (si présent) pour la balise \texttt{\textless{}select\textgreater{}}.
\end{itemize}
\url{http://dotmobo.github.io/django-crispy-forms.html}
\section{Conclusions}
Toute donnée entrée par l'utilisateur, quelle qu'elle soit, \textbf{doit} passer par une instance de \texttt{form}: qu'il s'agisse d'un formulaire HTML, d'un fichier CSV, d'un parser XML, \ldots
Dès que des informations "de l'extérieur" font mine d'arriver dans le périmètre de votre application, il convient d'appliquer immédiatement des principes de sécurité reconnus.