Easter notes on administration ,forms, migrations and models

This commit is contained in:
Fred Pauchet 2022-05-01 19:05:58 +02:00
parent c8f88779ff
commit 7d81286462
4 changed files with 144 additions and 123 deletions

View File

@ -3,13 +3,13 @@
Cette partie est tellement puissante et performante, qu'elle pourrait laisser penser qu'il est possible de réaliser une application complète rien qu'en configurant l'administration. C'est faux.
L'administration est une sorte de tour de contrôle évoluée, un \emph{back office} sans transpirer; elle se base sur le modèle de données programmé et construit dynamiquement les formulaires qui lui est associé.
Elle joue avec les clés primaires, étrangères, les champs et types de champs par \href{https://fr.wikipedia.org/wiki/Introspection}{introspection}, et présente tout ce qu'il faut pour avoir du \href{https://fr.wikipedia.org/wiki/CRUD}{CRUD}, c'est-à-dire tout ce qu'il faut pour ajouter, lister, modifier ou supprimer des informations.
Elle joue avec les clés primaires, étrangères, les champs et types de champs par \href{https://fr.wikipedia.org/wiki/Introspection}{introspection}, et présente tout ce qu'il faut pour avoir du \href{https://fr.wikipedia.org/wiki/CRUD}{CRUD} \index{CRUD} \footnote{\emph{Create-Read-Update-Delete}, c'est-à-dire le fonctionnement par défaut de beaucoup d'applications}, c'est-à-dire tout ce qu'il faut pour ajouter, lister, modifier ou supprimer des informations.
Son problème est qu'elle présente une courbe d'apprentissage asymptotique.
Il est \textbf{très} facile d'arriver rapidement à un bon résultat, au travers d'un périmètre de configuration relativement restreint.
Quoi que vous fassiez, il y a un moment où la courbe de paramétrage sera tellement ardue que vous aurez plus facile à développer ce que vous souhaitez ajouter en utilisant les autres concepts de Django.
Cette fonctionnalité doit rester dans les mains d'administrateurs ou de gestionnaires, et dans leurs mains à eux uniquement: il n'est pas question de donner des droits aux utilisateurs finaux (même si c'est extrêment tentant durant les premiers tours de roues).
Cette interface doit rester dans les mains d'administrateurs ou de gestionnaires, et dans leurs mains à eux uniquement: il n'est pas question de donner des droits aux utilisateurs finaux (même si c'est extrêment tentant durant les premiers tours de roues).
Indépendamment de la manière dont vous allez l'utiliser et la configurer, vous finirez par devoir développer une "vraie" application, destinée aux utilisateurs classiques, et répondant à leurs besoins uniquement.
Une bonne idée consiste à développer l'administration dans un premier temps, en \textbf{gardant en tête qu'il sera nécessaire de développer des concepts spécifiques}.
@ -17,11 +17,9 @@ Dans cet objectif, l'administration est un outil exceptionel, qui permet de vali
C'est aussi un excellent outil de prototypage et de preuve de concept.
Elle se base sur plusieurs couches que l'on a déjà (ou on va bientôt)
aborder (suivant le sens de lecture que vous préférez):
Elle se base sur plusieurs couches que l'on a déjà (ou on va bientôt) aborder (suivant le sens de lecture que vous préférez):
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le modèle de données
\item
@ -52,35 +50,35 @@ urlpatterns = [
Cette URL signifie que la partie \texttt{admin} est déjà active et accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}.
C'est le seul prérequis pour cette partie.
Chaque application nouvellement créée contient par défaut un fichier \texttt{admin.py}, dans lequel il est possible de déclarer quel ensemble de données sera accessible/éditable.
Chaque application nouvellement créée contient par défaut un fichier \texttt{admin.py}, dans lequel il est possible de déclarer les ensembles de données seront accessibles ou éditables.
Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits:
\begin{minted}{python}
# gwift/wish/models.py
# gwift/wish/models.py
from django.db import models
from django.db import models
class WishList(models.Model):
name = models.CharField(max_length=255)
class WishList(models.Model):
name = models.CharField(max_length=255)
class Item(models.Model):
name = models.CharField(max_length=255)
wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE)
class Item(models.Model):
name = models.CharField(max_length=255)
wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE)
\end{minted}
Nous pouvons facilement arriver au résultat suivant, en ajoutant
quelques lignes de configuration dans ce fichier \texttt{admin.py}:
\begin{minted}{python}
from django.contrib import admin
from django.contrib import admin
from .models import Item, WishList
from .models import Item, WishList
admin.site.register(Item)
admin.site.register(WishList)
admin.site.register(Item)
admin.site.register(WishList)
\end{minted}
@ -115,6 +113,8 @@ Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous auro
\includegraphics{images/django/django-site-admin-after-connection.png}
\caption{Administration}
\end{figure}
Ceci nous permet déjà d'ajouter des éléments (Items), des listes de souhaits, de visualiser les actions récentes, voire de gérer les autorisations attribuées aux utilisateurs, comme les membres du staff ou les administrateurs.
\section{Quelques conseils de base}
@ -186,7 +186,6 @@ Comme l'interface d'administration fonctionne (en trèèèès) gros comme un
CRUD auto-généré, on trouve par défaut la possibilité de :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Créer de nouveaux éléments
\item
@ -197,7 +196,7 @@ CRUD auto-généré, on trouve par défaut la possibilité de :
Supprimer un élément en particulier.
\end{enumerate}
Les affichages sont donc de deux types: en liste et par élément.
Les affichages sont donc de deux types: en liste et au détail.
Pour les affichages en liste, le plus simple consiste à jouer sur la propriété \texttt{list\_display}.
@ -208,18 +207,27 @@ Voir aussi comment personnaliser le fil d'Ariane ?
\section{Filtres}
Chaque liste permet de spécifier des filtres spécifiques; ceux-ci peuvent être:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
list\_filter
\item
filter\_horizontal
\item
filter\_vertical
\item
date\_hierarchy
\item \textbf{Appliqués à la liste} (\texttt{list\_filter})
\item \textbf{Horizontaux} (\texttt{filter\_horizontal})
\item \textbf{Verticaux} (\texttt{filter\_vertical})
\item \textbf{Temporels} (\texttt{date\_hierarchy
\end{enumerate}
\subsection{Appliqués à la liste}
\subsection{Horizontaux}
\subsection{Verticaux}
\subsection{Temporels}
\section{Permissions}
On l'a dit plus haut, il vaut mieux éviter de proposer un accès à l'administration à vos utilisateurs.
@ -249,13 +257,13 @@ Les relations 1-n sont implémentées au travers de formsets (que l'on a normale
L'implémentation consiste tout d'abord à définir le comportement du type d'objet référencé (la relation -N), puis à inclure cette définition au niveau du type d'objet référençant (la relation 1-).
\begin{minted}{python}
class WishInline(TabularInline):
model = Wish
class WishInline(TabularInline):
model = Wish
class Wishlist(admin.ModelAdmin):
...
inlines = [WishInline]
...
class Wishlist(admin.ModelAdmin):
...
inlines = [WishInline]
...
\end{minted}
Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist}) pourra directement gérer des relations multiples vers des souhaits.
@ -264,6 +272,10 @@ Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist}) pourra di
Parler de l'intégration de select2.
\section{Forms}
\section{Présentation}
Parler ici des \texttt{fieldsets} et montrer comment on peut regrouper des champs dans des groupes, ajouter un peu de JavaScript, ...

View File

@ -1,15 +1,14 @@
\chapter{Forms}
\includegraphics{images/xkcd-327.png}
Ou comment valider proprement des données entrantes.
\includegraphics{images/xkcd-327.png}
\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}
Ou comment valider proprement des données entrantes.
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.
@ -53,26 +52,28 @@ A compléter ;-)
\section{Dépendance avec le modèle}
Un \textbf{form} peut dépendre d'une autre classe Django.
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 django import forms
from wish.models import Wishlist
from wish.models import Wishlist
class WishlistCreateForm(forms.ModelForm):
class WishlistCreateForm(forms.ModelForm):
class Meta:
model = Wishlist
fields = ('name', 'description')
model = Wishlist
fields = ('name', 'description')
\end{minted}
De cette manière, notre form dépendra automatiquement des champs déjà déclarés dans la classe \texttt{Wishlist}.
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.
@ -86,19 +87,15 @@ On a d'un côté le \{\{ form.as\_p \}\} ou \{\{ form.as\_table \}\}, mais il y
\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.
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\hspace{0pt} 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.
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, ...
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)
@ -122,4 +119,5 @@ Pour chaque champ, crispy-forms va :
\section{Conclusions}
Toute donnée entrée par l'utilisateur \textbf{doit} passer par une instance de \texttt{form}.
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, ...
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.

View File

@ -3,8 +3,6 @@
Dans cette section, nous allons voir comment fonctionnent les migrations.
Lors d'une première approche, elles peuvent sembler un peu magiques, puisqu'elles centralisent un ensemble de modifications pouvant être répétées sur un schéma de données, en tenant compte de ce qui a déjà été appliqué et en vérifiant quelles migrations devaient encore l'être pour mettre l'application à niveau. Une analyse en profondeur montrera qu'elles ne sont pas plus complexes à suivre et à comprendre qu'un ensemble de fonctions de gestion appliquées à notre application.
La commande \texttt{sqldump}, qui nous présentera le schéma tel qu'il sera compris.
L'intégration des migrations a été réalisée dans la version 1.7 de Django.
Avant cela, il convenait de passer par une librairie tierce intitulée \href{https://south.readthedocs.io/en/latest}{South}.
@ -20,7 +18,7 @@ Bref, dans les années '80, il convenait de jouer ceci après s'être connecté
ALTER TABLE WishList ADD COLUMN Description nvarchar(MAX);
\end{minted}
Et là, nous nous rappelons qu'un utilisateur tourne sur Oracle et pas sur MySQL, et qu'il a donc besoin de son propre script d'exécution, parce que le type du nouveau champ n'est pas exactement le même entre les deux moteurs différents:
Et là, nous nous rappelons qu'un client tourne sur Oracle et pas sur MySQL, et qu'il a donc besoin de son propre script d'exécution, parce que le type du nouveau champ n'est pas exactement le même entre deux moteurs différents:
\begin{minted}{sql}
-- Firebird
@ -38,26 +36,25 @@ En bref, les problèmes suivants apparaissent très rapidement:
\begin{enumerate}
\item
Aucune autonomie: il est nécessaire d'avoir les compétences d'une
\textbf{Manque d'autonomie}: il est nécessaire d'avoir les compétences d'une
personne tierce pour avancer ou de disposer des droits
administrateurs,
\item
Aucune automatisation possible, à moins d'écrire un programme, qu'il
\textbf{Manque d'automatisation possible}, à moins d'écrire un programme, qu'il
faudra également maintenir et intégrer au niveau des tests
\item
Nécessité de maintenir autant de scripts différents qu'il y a de
\textbf{Nécessité de maintenir des scripts} différents, en fonction des
moteurs de base de données supportés
\item
Aucune possibilité de vérifier si le script a déjà été exécuté ou non,
à moins, à nouveau, de maintenir un programme supplémentaire.
\textbf{Manque de vérification} si un script a déjà été exécuté ou non,
à moins, à nouveau, de maintenir un programme ou une table supplémentaire.
\end{enumerate}
\section{Fonctionnement général}
Le moteur de migrations résout la plupart de ces soucis: le framework embarque ses propres applications, dont les migrations, qui gèrent elles-mêmes l'arbre de dépendances entre les modifications devant être
appliquées.
Le moteur de migrations résout la plupart de ces soucis: le framework embarque ses propres applications, dont les migrations, qui gèrent elles-mêmes l'arbre de dépendances entre les modifications qui doivent être appliquées.
Pour reprendre un des premiers exemples, nous avions créé un modèle contenant deux classes, qui correspondent chacun à une table dans un modèle relationnel:
Pour reprendre un de nos exemples précédents, nous avions créé un modèle contenant deux classes, qui correspondent chacun à une table dans un modèle relationnel:
\begin{minted}{python}
class Category(models.Model):
@ -81,60 +78,63 @@ class Book(models.Model):
category = models.ManyManyField(Category, on_delete=models.CASCADE)
\end{minted}
Chronologiquement, cela nous a donné
Chronologiquement, cela nous a donné une première migration consistant à créer le modèle initial, suivie d'une seconde migration après que nous ayons modifié le modèle pour autoriser des relations multiples.
migrations successives, à appliquer pour que la structure relationnelle corresponde aux attentes du modèle Django:
\begin{enumerate}
\item Une première migration consistant à créer le modèle initial
\item Suivie d'une seconde migration après que nous ayons modifié le modèle pour autoriser des relations multiples
\end{enumerate}
\begin{minted}{python}
# library/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="Book",
fields=[
(
"id",
models.BigAutoField(
# library/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
),
(
"title",
models.CharField(max_length=255)
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="library.category",
("name", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="Book",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
),
],
),
]
(
"title",
models.CharField(max_length=255)
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="library.category",
),
),
],
),
]
\end{minted}
\begin{itemize}
@ -309,12 +309,10 @@ migrations soient appliquées \textbf{avant} que le code ne soit déployé;
l'idéal étant que ces deux opérations soient réalisées de manière
atomique, avec un \emph{rollback} si une anomalie était détectée.
En allant
Pour éviter ce type d'erreurs, plusieurs stratégies peuvent être
appliquées:
intégrer ici un point sur les updates db - voir designing data-intensive
TODO intégrer ici un point sur les updates db - voir designing data-intensive
applications.
Toujours dans une optique de centralisation, les migrations sont
@ -398,6 +396,8 @@ pas encore été appliquées:
[X] 0001_initial
\end{verbatim}
Nous voyons que parmi toutes les migrations déjà enregistrées au niveau du projet, seule la migration \texttt{0003\_book\_summary} n'a pas encore été appliquée sur ce schéma-ci.
\section{Squash}
Finalement, lorsque vous développez sur votre propre branche (cf. \protect\hyperlink{git}{???}), vous serez peut-être tentés de créer plusieurs migrations en fonction de l'évolution de ce que vous mettez en place. Dans ce cas précis, il peut être intéressant d'utiliser la méthode \texttt{squashmigrations}, qui permet \emph{d'aplatir} plusieurs fichiers en un seul.
@ -510,13 +510,12 @@ class Migration(migrations.Migration):
\section{Réinitialisation de migrations}
\href{https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html}{reset
migrations}.
\begin{quote}
\begin{verbatim}
En gros, soit on supprime toutes les migrations (en conservant le fichier __init__.py), soit on réinitialise proprement les migrations avec un --fake-initial (sous réserve que toutes les personnes qui utilisent déjà le projet s'y conforment... Ce qui n'est pas gagné.
Pour repartir de notre exemple ci-dessus, nous avions un modèle reprenant quelques classes, saupoudrées de propriétés décrivant nos différents champs. Pour être prise en compte par le moteur de base de données, chaque modification doit être
\end{verbatim}
\end{quote}
En résumé:
\begin{enumerate}
\item Soit on supprime toutes les migrations (en conservant le fichier \texttt{\_\_init\_\_.py})
\item Soit on réinitialise proprement les migrations avec un \texttt{--fake-initial} (sous réserve que toutes les personnes qui utilisent déjà le projet s'y conforment... Ce qui n'est pas gagné.
\end{enumerate}

View File

@ -241,6 +241,18 @@ class Book(models.Model):
Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition.
\section{Shell}
Le \texttt{shell} est un environnement REPL \index{REPL} identique à ce que l'interpréteur Python offre par défaut, connecté à la base de données, qui permet de :
\begin{enumerate}
\item Tester des comportements spécifiques
\item Instancier des enregistrements provenant de la base de données
\item Voire, exceptionnellement, d'analyser un soucis en production.
\end{enumerate}
Il se démarre grâce à la commande \texttt{python manage.py shell}, et donne un accès intuitif \footnote{Pour un développeur...} à l'ensemble des informations disponibles.
\subsection{Accès aux relations}
\begin{minted}{python}