Working a little bit on migrations, models, etc.
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-04-25 19:12:16 +02:00
parent 208ea90e2f
commit ee76783f86
14 changed files with 2026 additions and 2713 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
\chapter{Administration}
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.
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).
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}.
Dans cet objectif, l'administration est un outil exceptionel, qui permet de valider un modèle, de créer des objets rapidement et de valider les liens qui existent entre eux.
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):
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le modèle de données
\item
Les validateurs
\item
Les formulaires
\item
Les widgets
\end{enumerate}
\section{Le modèle de données}
Comme expliqué ci-dessus, le modèle de données est constité d'un ensemble de champs typés et de relations. L'administration permet de décrire les données qui peuvent être modifiées, en y associant un ensemble (basique) de permissions.
Si vous vous rappelez de l'application que nous avions créée dans la première partie, les URLs reprenaient déjà la partie suivante:

522
chapters/migrations.tex Normal file
View File

@ -0,0 +1,522 @@
\chapter{Migrations}
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}.
Prenons l'exemple de notre liste de souhaits; nous nous rendons (bêtement) compte que nous avons oublié d'ajouter un champ de \texttt{description} à une liste.
Historiquement, cette action nécessitait l'intervention d'un administrateur système ou d'une personne
ayant accès au schéma de la base de données, à partir duquel ce-dit utilisateur pouvait jouer manuellement un script SQL. \index{SQL}
Cet enchaînement d'étapes nécessitait une bonne coordination d'équipe, mais également une bonne confiance dans les scripts à exécuter.
Et souvenez-vous (cf. ref-à-insérer), que l'ensemble des actions doit être répétable et automatisable.
Bref, dans les années '80, il convenait de jouer ceci après s'être connecté au serveur de base de données:
\begin{minted}{sql}
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:
\begin{minted}{sql}
-- Firebird
ALTER TABLE Category ALTER COLUMN Name type varchar(2000)
-- MSSQL
ALTER TABLE Category ALTER Column Name varchar(2000)
-- Oracle
ALTER TABLE Category MODIFY Name varchar2(2000
\end{minted}
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
personne tierce pour avancer ou de disposer des droits
administrateurs,
\item
Aucune 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
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.
\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.
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:
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
\end{minted}
Nous avions ensuite modifié la clé de liaison, pour permettre d'associer plusieurs catégories à un même livre, et inversément:
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category, on_delete=models.CASCADE)
\end{minted}
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{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(
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}
\item
La migration crée un nouveau modèle intitulé "Category", possédant un
champ \texttt{id} (auto-défini, puisque nous n'avons rien fait), ainsi
qu'un champ \texttt{name} de type texte et d'une longue maximale de
255 caractères.
\item
Elle crée un deuxième modèle intitulé "Book", possédant trois champs:
son identifiant auto-généré \texttt{id}, son titre \texttt{title} et
sa relation vers une catégorie, au travers du champ \texttt{category}.
\end{itemize}
Un outil comme \href{https://sqlitebrowser.org/}{DB Browser For SQLite} nous donne la structure suivante:
\includegraphics{images/db/migrations-0001-to-0002.png}
La représentation au niveau de la base de données est la suivante:
\includegraphics{images/db/link-book-category-fk.drawio.png}
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category)
\end{minted}
Vous noterez que l'attribut \texttt{on\_delete} n'est plus nécessaire.
Après cette modification, la migration résultante à appliquer correspondra à ceci. En SQL, un champ de type \texttt{ManyToMany} ne peut qu'être représenté par une table intermédiaire.
Ce qu'applique la migration en supprimant le champ liant initialement un livre à une catégorie et en ajoutant une nouvelle table de liaison.
\begin{minted}{python}
# library/migrations/0002_remove_book_category_book_category.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.Category'),
),
]
\end{minted}
\begin{itemize}
\item
La migration supprime l'ancienne clé étrangère ...
\item
... et ajoute une nouvelle table, permettant de lier nos catégories à nos livres.
\end{itemize}
\includegraphics{images/db/migrations-0002-many-to-many.png}
Nous obtenons à présent la représentation suivante en base de données:
\includegraphics{images/db/link-book-category-m2m.drawio.png}
\section{Graph de dépendances}
Lorsqu'une migration applique une modification au schéma d'une base de données, il est évident qu'elle ne peut pas être appliquée dans n'importe quel ordre ou à n'importe quel moment.
Dès la création d'un nouveau projet, avec une configuration par défaut et même sans avoir ajouté d'applications, Django proposera immédiatement d'appliquer les migrations des applications \textbf{admin},
\textbf{auth}, \textbf{contenttypes} et \textbf{sessions}, qui font partie du coeur du système, et qui se trouvent respectivement aux emplacements suivants:
\begin{itemize}
\item
\textbf{admin}: \texttt{site-packages/django/contrib/admin/migrations}
\item
\textbf{auth}: \texttt{site-packages/django/contrib/auth/migrations}
\item
\textbf{contenttypes}:
\texttt{site-packages/django/contrib/contenttypes/migrations}
\item
\textbf{sessions}:
\texttt{site-packages/django/contrib/sessions/migrations}
\end{itemize}
Ceci est dû au fait que, toujours par défaut, ces applications sont reprises au niveau de la configuration d'un nouveau projet, dans le fichier \texttt{settings.py}:
\begin{minted}{python}
[snip]
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
[snip]
\end{minted}
Dès que nous les appliquerons, nous recevrons les messages suivants:
\begin{verbatim}
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, library, sessions, world
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
\end{verbatim}
Cet ordre est défini au niveau de la propriété \texttt{dependencies}, que l'on retrouve au niveau de chaque description de migration.
En explorant les paquets qui se trouvent au niveau des répertoires et en analysant les dépendances décrites au niveau de chaque action de migration, on arrive au schéma suivant, qui est un graph dirigé acyclique:
\includegraphics{images/db/migrations_auth_admin_contenttypes_sessions.png}
\section{Sous le capot}
Une migration consiste à appliquer un ensemble de modifications (ou
\textbf{opérations}), qui exercent un ensemble de transformations, pour
que le schéma de base de données corresponde au modèle de l'application
sous-jacente.
Les migrations (comprendre les "\emph{migrations du schéma de base de
données}") sont intimement liées à la représentation d'un contexte
fonctionnel: l'ajout d'une nouvelle information, d'un nouveau champ ou
d'une nouvelle fonction peut s'accompagner de tables de données à mettre
à jour ou de champs à étendre. Il est primordial que la structure de la
base de données corresponde à ce à quoi l'application s'attend, sans
quoi la probabilité que l'utilisateur tombe sur une erreur de type
\texttt{django.db.utils.OperationalError} est (très) grande.
Typiquement, après avoir ajouté un nouveau champ \texttt{summary} à
chacun de nos livres, et sans avoir appliqué de migrations, nous tombons
sur ceci:
\begin{verbatim}
>>> from library.models import Book
>>> Book.objects.all()
Traceback (most recent call last):
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/sqlite3/base.py", line 416, in execute
return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such column: library_book.summary
\end{verbatim}
Pour éviter ce type d'erreurs, il est impératif que les nouvelles
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
applications.
Toujours dans une optique de centralisation, les migrations sont
directement embarquées au niveau du code, et doivent faire partie du
dépôt central de sources. Le développeur s'occupe de créer les
migrations en fonction des actions à entreprendre; ces migrations
peuvent être retravaillées, \emph{squashées}, \ldots\hspace{0pt} et
feront partie intégrante du processus de mise à jour de l'application.
A noter que les migrations n'appliqueront de modifications que si le
schéma est impacté. Ajouter une propriété \texttt{related\_name} sur une
ForeignKey n'engendrera aucune nouvelle action de migration, puisque ce
type d'action ne s'applique que sur l'ORM, et pas directement sur la
base de données: au niveau des tables, rien ne change. Seul le code et
le modèle sont impactés.
Une migration est donc une classe Python, présentant \emph{a minima}
deux propriétés:
\begin{enumerate}
\item
\texttt{dependencies}, qui décrit les opérations précédentes devant obligatoirement avoir été appliquées
\item
\texttt{operations}, qui consiste à décrire précisément ce qui doit être exécuté.
\end{enumerate}
Pour reprendre notre exemple d'ajout d'un champ \texttt{description} sur le modèle \texttt{WishList}, la migration ressemblera à ceci:
\begin{minted}{python}
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('gwift', '0004_name_value'),
]
operations = [
migrations.AddField(
model_name='wishlist',
name='description',
field=models.TextField(default="", null=True)
preserve_default=False,
),
]
\end{minted}
\section{Liste des migrations appliquées}
L'option \texttt{showmigrations} de \texttt{manage.py} permet de lister
toutes les migrations du projet, et d'identifier celles qui n'auraient
pas encore été appliquées:
\begin{verbatim}
$ python manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
[X] 0010_alter_group_name_max_length
[X] 0011_update_proxy_permissions
[X] 0012_alter_user_first_name_max_length
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
library
[X] 0001_initial
[X] 0002_remove_book_category_book_category
[ ] 0003_book_summary
sessions
[X] 0001_initial
\end{verbatim}
\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.
Nous partons dans deux migrations suivantes:
\begin{minted}{python}
# library/migrations/0002_remove_book_category.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.Category'),
),
]
\end{minted}
\begin{minted}{python}
# library/migrations/0003_book_summary.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0002_remove_book_category_book_category'),
]
operations = [
migrations.AddField(
model_name='book',
name='summary',
field=models.TextField(blank=True),
),
]
\end{minted}
La commande \texttt{python\ manage.py\ squashmigrations\ library\ 0002\ 0003} appliquera une fusion entre les migrations numérotées \texttt{0002} et \texttt{0003}:
\begin{verbatim}
$ python manage.py squashmigrations library 0002 0003
Will squash the following migrations:
- 0002_remove_book_category_book_category
- 0003_book_summary
Do you wish to proceed? [yN] y
Optimizing...
No optimizations possible.
Created new squashed migration
/home/fred/Sources/gwlib/library/migrations/0002_remove_book_category_book_cat
egory_squashed_0003_book_summary.py
You should commit this migration but leave the old ones in place;
the new migration will be used for new installs. Once you are sure
all instances of the codebase have applied the migrations you squashed,
you can delete them
\end{verbatim}
Dans le cas où vous développez proprement (bis), il est sauf de purement et simplement supprimer les anciens fichiers; dans le cas où il pourrait exister au moins une instance ayant appliqué ces migrations, les anciens
\textbf{ne peuvent surtout pas être modifiés}.
Nous avons à présent un nouveau fichier intitulé \texttt{0002\_remove\_book\_category\_book\_category\_squashed\_0003\_book\_summary}:
\begin{minted}{python}
$ cat
library/migrations/0002_remove_book_category_book_category_squashed_0003_book_
summary.py
# Generated by Django 4.0.3 on 2022-03-15 18:01
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
('library', '0002_remove_book_category_book_category'),
('library', '0003_book_summary')]
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.category'),
),
migrations.AddField(
model_name='book',
name='summary',
field=models.TextField(blank=True),
),
]
\end{minted}
\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}

574
chapters/models.tex Normal file
View File

@ -0,0 +1,574 @@
\chapter{Modélisation}
Ce chapitre aborde la modélisation des objets et les options qui y sont liées.
Avec Django, la modélisation est en lien direct avec la conception et le stockage, sous forme d'une base de données relationnelle, et la manière dont ces données s'agencent et communiquent entre elles.
Cette modélisation va ériger les premières pierres de votre édifice.
\begin{quote}
\emph{Le modèle n'est qu'une grande hypothèque. Il se base sur des choix conscients et inconscients, et dans chacun de ces choix se cachent nos propres perceptions qui résultent de qui nous sommes, de nos connaissances, de nos profils scientifiques et de tant d'autres choses.}
--- Aurélie Jean De l'autre côté de la machine
\end{quote}
Comme expliqué par Aurélie Jean cite:{[}other\_side{]}, "\emph{toute modélisation reste une approximation de la réalité}".
Plus tard dans ce chapitre, nous expliquerons les bonnes pratiques à suivre pour faire évoluer ces biais.
Django utilise un paradigme de persistence des données de type \href{https://fr.wikipedia.org/wiki/Mapping_objet-relationnel}{ORM} - c'est-à-dire que chaque type d'objet manipulé peut s'apparenter à une
table SQL, tout en respectant une approche propre à la programmation orientée object. Plus spécifiquement, l'ORM de Django suit le patron de conception \href{https://en.wikipedia.org/wiki/Active_record_pattern}{Active Records}, comme le font par exemple \href{https://rubyonrails.org/}{Rails} pour Ruby ou \href{https://docs.microsoft.com/fr-fr/ef/}{EntityFramework} pour .Net.
Le modèle de données de Django est sans doute la (seule ?) partie qui soit tellement couplée au framework qu'un changement à ce niveau nécessitera une refonte complète de beaucoup d'autres briques de vos applications; là où un pattern de type \href{https://www.martinfowler.com/eaaCatalog/repository.html}{Repository} permettrait justement de découpler le modèle des données de l'accès à ces mêmes données, un pattern Active Record lie de manière extrêmement forte le modèle à sa persistence. Architecturalement, c'est sans doute la plus grosse faiblesse de Django, à tel point que \textbf{ne pas utiliser cette brique de fonctionnalités} peut remettre en question le choix du framework.
Conceptuellement, c'est pourtant la manière de faire qui permettra d'avoir quelque chose à présenter très rapidement: à partir du moment où vous aurez un modèle de données, vous aurez accès, grâce à cet ORM à:
\begin{enumerate}
\item
Des migrations de données et la possibilité de faire évoluer votre modèle,
\item
Une abstraction entre votre modélisation et la manière dont les données sont représentées \emph{via} un moteur de base de données relationnelles,
\item
Une interface d'administration auto-générée
\item
Un mécanisme de formulaires HTML qui soit complet, pratique à utiliser, orienté objet et facile à faire évoluer,
\item
Une définition des notions d'héritage (tout en restant dans une forme d'héritage simple).
\end{enumerate}
Comme tout ceci reste au niveau du code, cela suit également la méthodologie des douze facteurs, concernant la minimisation des divergences entre environnements d'exécution: comme tout se trouve au niveau du code, il n'est plus nécessaire d'avoir un DBA qui doive démarrer un script sur un serveur au moment de la mise à jour, de recevoir une release note de 512 pages en PDF reprenant les modifications ou de nécessiter l'intervention de trois équipes différentes lors d'une modification majeure du code.
Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande.
\section{Active Records}
Il est important de noter que l'implémentation d'Active Records reste une forme hybride entre une structure de données brutes et une classe:
\begin{itemize}
\item
Une classe va exposer ses données derrière une forme d'abstraction et n'exposer que les fonctions qui opèrent sur ces données,
\item
Une structure de données ne va exposer que ses champs et propriétés, et ne va pas avoir de functions significatives.
\end{itemize}
L'exemple ci-dessous présente trois structure de données, qui exposent chacune leurs propres champs:
\begin{minted}{Python}
class Square:
def __init__(self, top_left, side):
self.top_left = top_left
self.side = side
class Rectangle:
def __init__(self, top_left, height, width):
self.top_left = top_left
self.height = height
self.width = width
class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius
\end{minted}
Si nous souhaitons ajouter une fonctionnalité permettant de calculer l'aire pour chacune de ces structures, nous aurons deux possibilités:
\begin{enumerate}
\item
Soit ajouter une classe de \emph{visite} qui ajoute cette fonction de calcul d'aire
\item
Soit modifier notre modèle pour que chaque structure hérite d'une classe de type \texttt{Shape}, qui implémentera elle-même ce calcul d'aire.
\end{enumerate}
Dans le premier cas, nous pouvons procéder de la manière suivante:
\begin{minted}{python}
class Geometry:
PI = 3.141592653589793
def area(self, shape):
if isinstance(shape, Square):
return shape.side * shape.side
if isinstance(shape, Rectangle):
return shape.height * shape.width
if isinstance(shape, Circle):
return PI * shape.radius**2
raise NoSuchShapeException()
\end{minted}
Dans le second cas, l'implémentation pourrait évoluer de la manière suivante:
\begin{minted}{python}
class Shape:
def area(self):
pass
class Square(Shape):
def __init__(self, top_left, side):
self.__top_left = top_left
self.__side = side
def area(self):
return self.__side * self.__side
class Rectangle(Shape):
def __init__(self, top_left, height, width):
self.__top_left = top_left
self.__height = height
self.__width = width
def area(self):
return self.__height * self.__width
class Circle(Shape):
def __init__(self, center, radius):
self.__center = center
self.__radius = radius
def area(self):
PI = 3.141592653589793
return PI * self.__radius**2
\end{minted}
Une structure de données peut être rendue abstraite au travers des notions de programmation orientée objet.
Dans l'exemple géométrique ci-dessus, repris de \cite[pp. 95-97]{clean_code}, l'accessibilité des champs devient restreinte, tandis que la fonction \texttt{area()} bascule comme méthode d'instance plutôt que de l'isoler au niveau d'un visiteur.
Nous ajoutons une abstraction au niveau des formes grâce à un héritage sur la classe \texttt{Shape}; indépendamment de ce que nous manipulerons, nous aurons la possibilité de calculer son aire.
Une structure de données permet de facilement gérer des champs et des propriétés, tandis qu'une classe gère et facilite l'ajout de fonctions et de méthodes.
Le problème d'Active Records est que chaque classe s'apparente à une table SQL et revient donc à gérer des \emph{DTO} ou \emph{Data Transfer Object}, c'est-à-dire des objets de correspondance pure et simple entre
les champs de la base de données et les propriétés de la programmation orientée objet, c'est-à-dire également des classes sans fonctions.
Or, chaque classe a également la possibilité d'exposer des possibilités d'interactions au niveau de la persistence, en \href{https://docs.djangoproject.com/en/stable/ref/models/instances/\#django.db.models.Model.save}{enregistrant ses propres données} ou en en autorisant leur \href{https://docs.djangoproject.com/en/stable/ref/models/instances/\#deleting-objects}{suppression}.
Nous arrivons alors à un modèle hybride, mélangeant des structures de données et des classes d'abstraction, ce qui restera parfaitement viable tant que l'on garde ces principes en tête et que l'on se prépare à une
éventuelle réécriture du code.
Lors de l'analyse d'une classe de modèle, nous pouvons voir que Django exige un héritage de la classe \texttt{django.db.models.Model}.
Nous pouvons regarder les propriétés définies dans cette classe en analysant le fichier
\texttt{lib\textbackslash{}site-packages\textbackslash{}django\textbackslash{}models\textbackslash{}base.py}.
Outre que \texttt{models.Model} hérite de \texttt{ModelBase} au travers de \href{https://pypi.python.org/pypi/six}{six} pour la rétrocompatibilité vers Python 2.7, cet héritage apporte notamment les fonctions \texttt{save()}, \texttt{clean()}, \texttt{delete()}, ...
En résumé, toutes les méthodes qui font qu'une instance sait \textbf{comment} interagir avec la base de données.
\section{Types de champs, relations et clés étrangères}
Nous l'avons vu plus tôt, Python est un langage dynamique et fortement typé.
Django, de son côté, ajoute une couche de typage statique exigé par le lien sous-jacent avec le moteur de base de données relationnelle.
Dans le domaine des bases de données relationnelles, un point d'attention est de toujours disposer d'une clé primaire pour nos enregistrements.
Si aucune clé primaire n'est spécifiée, Django s'occupera d'en ajouter une automatiquement et la nommera (par
convention) \texttt{id}.
Elle sera ainsi accessible autant par cette propriété que par la propriété \texttt{pk}.
Chaque champ du modèle est donc typé et lié, soit à une primitive, soit à une autre instance au travers de sa clé d'identification.
Grâce à toutes ces informations, nous sommes en mesure de représenter facilement des livres liés à des catégories:
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
\end{minted}
Par défaut, et si aucune propriété ne dispose d'un attribut \texttt{primary\_key=True}, Django s'occupera d'ajouter un champ \texttt{id} grâce à son héritage de la classe \texttt{models.Model}.
Les autres champs nous permettent d'identifier une catégorie (\texttt{Category}) par un nom (\texttt{name}), tandis qu'un livre (\texttt{Book}) le sera par ses propriétés \texttt{title} et une clé de relation vers une catégorie. Un livre est donc lié à une catégorie, tandis qu'une catégorie est associée à plusieurs livres.
\includegraphics{diagrams/books-foreign-keys-example.drawio.png}
A présent que notre structure dispose de sa modélisation, il nous faut informer le moteur de base de données de créer la structure correspondance:
\begin{verbatim}
$ python manage.py makemigrations
Migrations for 'library':
library/migrations/0001_initial.py
- Create model Category
- Create model Book
\end{verbatim}
Cette étape créera un fichier différentiel, explicitant les modifications à appliquer à la structure de données pour rester en corrélation avec la modélisation de notre application.
Nous pouvons écrire un premier code d'initialisation de la manière suivante:
\begin{minted}{python}
from library.models import Book, Category
movies = Category.objects.create(name="Adaptations au cinéma")
medieval = Category.objects.create(name="Médiéval-Fantastique")
science_fiction = Category.objects.create(name="Sciences-fiction")
computers = Category.objects.create(name="Sciences Informatiques")
books = {
"Harry Potter": movies,
"The Great Gatsby": movies,
"Dune": science_fiction,
"H2G2": science_fiction,
"Ender's Game": science_fiction,
"Le seigneur des anneaux": medieval,
"L'Assassin Royal", medieval,
"Clean code": computers,
"Designing Data-Intensive Applications": computers
}
for book_title, category in books.items:
Book.objects.create(name=book_title, category=category)
\end{minted}
Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs catégories:
\begin{itemize}
\item
\emph{Dune} a été adapté au cinéma en 1973 et en 2021, de même que \emph{Le Seigneur des Anneaux}.
Ces deux titres (au moins) peuvent appartenir à deux catégories distinctes.
\item
Pour \emph{The Great Gatsby}, c'est l'inverse: nous l'avons initialement classé comme film, mais le livre existe depuis 1925.
\item
Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie spéciale "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga \emph{Harry Potter} et ses dérivés.
\end{itemize}
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour qu'une occurrence puisse être liée à plusieurs catégories.
Au lieu d'utiliser un champ de type \texttt{ForeignKey}, nous utiliserons un champ de type \texttt{ManyToMany}, c'est-à-dire qu'à présent, un livre pourra être lié à plusieurs catégories, et qu'inversément, une même catégorie pourra être liée à plusieurs livres.
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category, on_delete=models.CASCADE)
\end{minted}
Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition.
\subsection{Accès aux relations}
\begin{minted}{python}
# wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist)
\end{minted}
Depuis le code, à partir de l'instance de la classe \texttt{Item}, on peut donc accéder à la liste en appelant la propriété \texttt{wishlist} de notre instance. \textbf{A contrario}, depuis une instance de type
\texttt{Wishlist}, on peut accéder à tous les éléments liés grâce à \texttt{\textless{}nom\ de\ la\ propriété\textgreater{}\_set}; ici \texttt{item\_set}.
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut \texttt{related\_name} afin de nommer la relation inverse.
\begin{minted}{python}
# wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist, related_name='items')
\end{minted}
Si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse.
Pour palier à ce problème, nous fixons une valeur à l'attribut \texttt{related\_name}. Par facilité (et par conventions), prenez l'habitude de toujours ajouter cet attribut: votre modèle gagnera en cohérence et en lisibilité. Si cette relation inverse n'est pas nécessaire, il est possible de l'indiquer (par convention) au travers de l'attribut \texttt{related\_name="+"}.
A partir de maintenant, nous pouvons accéder à nos propriétés de la manière suivante:
\begin{verbatim}
# python manage.py shell
>>> from wish.models import Wishlist, Item
>>> wishlist = Wishlist.create('Liste de test', 'description')
>>> item = Item.create('Element de test', 'description', w)
>>>
>>> item.wishlist
<Wishlist: Wishlist object>
>>>
>>> wishlist.items.all()
[<Item: Item object>]
\end{verbatim}
\subsection{Choix}
Voir \href{https://girlthatlovestocode.com/django-model}{ici}
\begin{minted}{python}
class Runner(models.Model):
# this is new:
class Zone(models.IntegerChoices):
ZONE_1 = 1, 'Less than 3.10'
ZONE_2 = 2, 'Less than 3.25'
ZONE_3 = 3, 'Less than 3.45'
ZONE_4 = 4, 'Less than 4 hours'
ZONE_5 = 5, 'More than 4 hours'
name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField()
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
start_zone = models.PositiveSmallIntegerField( # this is new
choices=Zone.choices,
default=Zone.ZONE_5,
help_text="What was your best time on the marathon in last 2 years?"
)
\end{minted}
\section{Validateurs}
\section{Constructeurs}
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode \texttt{init}: créez plutôt une méthode static de type \texttt{create()}, en y associant les paramètres obligatoires ou
souhaités.
Mieux encore: on pourrait passer par un \texttt{ModelManager} pour limiter le couplage; l'accès à une information stockée en base de données ne se ferait dès lors qu'au travers de cette instance et pas
directement au travers du modèle.
De cette manière, on limite le couplage des classes et on centralise l'accès.
\begin{minted}{python}
class ItemManager(...):
(de mémoire, je ne sais plus exactement)
\end{minted}
\section{Jointures, compositions et filtres}
Pour appliquer une jointure sur un modèle, nous pouvons passer par les méthodes \texttt{select\_related} et \texttt{prefetch\_related}.
Il faut cependant faire \textbf{très} attention au prefetch related, qui fonctionne en fait comme une grosse requête dans laquelle nous trouvons un \texttt{IN\ (...)}.
Càd que Django va récupérer tous les objets demandés initialement par le queryset, pour ensuite prendre
toutes les clés primaires, pour finalement faire une deuxième requête et récupérer les relations externes.
Au final, si votre premier queryset est relativement grand (nous parlons de 1000 à 2000 éléments, en fonction du moteur de base de données), la seconde requête va planter et vous obtiendrez une exception de type \texttt{django.db.utils.OperationalError:\ too\ many\ SQL\ variables}.
Nous pourrions penser qu'utiliser un itérateur permettrait de combiner les deux, mais ce n'est pas le cas...
Comme l'indique la documentation:
\begin{verbatim}
Note that if you use iterator() to run the query, prefetch_related() calls will be ignored since these two optimizations do not make sense together.
\end{verbatim}
Ajouter un itérateur va en fait forcer le code à parcourir chaque élément de la liste, pour l'évaluer. Il y aura donc (à nouveau) autant de requêtes qu'il y a d'éléments, ce que nous cherchons à éviter.
\begin{minted}{python}
informations = (
<MyObject>.objects.filter(<my_criteria>)
.select_related(<related_field>)
.prefetch_related(<related_field>)
.iterator(chunk_size=1000)
)
\end{minted}
DANGER: Les requêtes sont sensibles à la casse, \textbf{même} si le
moteur de base de données ne l'est pas. C'est notamment le cas pour
Microsoft SQL Server; faire une recherche directement via les outils de
Microsoft ne retournera pas obligatoirement les mêmes résultats que les
managers, qui seront beaucoup plus tatillons sur la qualité des
recherches par rapport aux filtres paramétrés en entrée.
\begin{verbatim}
Pour un `AND`, il suffit de chaîner les conditions. ** trouver un exemple ici ** :-)
\end{verbatim}
\begin{verbatim}
Mais en gros : bidule.objects.filter(condition1, condition2)
\end{verbatim}
\begin{verbatim}
Il existe deux autres options : combiner deux querysets avec l'opérateur `&` ou combiner des Q objects avec ce même opérateur.
\end{verbatim}
Soit encore combiner des filtres:
\begin{minted}{python}
from core.models import Wish
Wish.objects
Wish.objects.filter(name__icontains="test").filter(name__icontains="too")
\end{minted}
\begin{itemize}
\item
Ca, c'est notre manager.
\item
Et là, on chaîne les requêtes pour composer une recherche sur tous les
souhaits dont le nom contient (avec une casse insensible) la chaîne
"test" et dont le nom contient la chaîne "too".
\end{itemize}
Pour un 'OR', on a deux options :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Soit passer par deux querysets, typiuqment
\texttt{queryset1\ \textbar{}\ queryset2}
\item
Soit passer par des \texttt{Q\ objects}, que l'on trouve dans le
namespace \texttt{django.db.models}.
\end{enumerate}
\begin{minted}{python}
from django.db.models import Q
condition1 = Q(...)
condition2 = Q(...)
bidule.objects.filter(condition1 | condition2)
\end{minted}
L'opérateur inverse (\emph{NOT})
Idem que ci-dessus : soit on utilise la méthode \texttt{exclude} sur le
queryset, soit l'opérateur \texttt{\textasciitilde{}} sur un Q object;
\section{Optimisation}
\subsection{N+1 Queries}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Prefetch
\item
select\_related
\end{enumerate}
\subsection{Unicité}
\subsection{Indices}
\section{Agrégation et annotations}
\url{https://docs.djangoproject.com/en/3.1/topics/db/aggregation/}
\section{Métamodèle et introspection}
Comme chaque classe héritant de \texttt{models.Model} possède une propriété \texttt{objects}.
Comme on l'a vu dans la section \textbf{Jouons un peu avec la console}, cette propriété permet d'accéder
aux objects persistants dans la base de données, au travers d'un \texttt{ModelManager}.
En plus de cela, il faut bien tenir compte des propriétés \texttt{Meta} de la classe: si elle contient déjà un ordre par défaut, celui-ci sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe.
\begin{minted}{python}
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',)
\end{minted}
Nous définissons un ordre par défaut, directement au niveau du modèle.
Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre (la méthode \texttt{order\_by} existe et peut être chaînée à n'importe quel \emph{queryset}). D'où l'intérêt de tester ce type de comportement, dans la mesure où un \texttt{top\ 1} dans votre code pourrait être modifié simplement par cette petite information.
Pour sélectionner un objet au pif: \texttt{return\ Category.objects.order\_by("?").first()}
Les propriétés de la classe Meta les plus utiles sont les suivates:
\begin{itemize}
\item
\texttt{ordering} pour spécifier un ordre de récupération spécifique.
\item
\texttt{verbose\_name} pour indiquer le nom à utiliser au singulier
pour définir votre classe
\item
\texttt{verbose\_name\_plural}, pour le pluriel.
\item
\texttt{contraints} (Voir \href{https://girlthatlovestocode.com/django-model}{ici}-), par exemple
\end{itemize}
\begin{minted}{python}
constraints = [ # constraints added
models.CheckConstraint(check=models.Q(year_born__lte=datetime.date
.today().year-18), name='will_be_of_age'),
]
\end{minted}
\section{Querysets et managers}
\begin{itemize}
\item
\url{http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm}
\item
\url{https://docs.djangoproject.com/en/1.9/ref/models/querysets/\#django.db.models.query.QuerySet.iterator}
\item
\url{http://blog.etianen.com/blog/2013/06/08/django-querysets/}
\end{itemize}
L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants:
\begin{itemize}
\item
Les \texttt{managers}, qui consistent en un point d'entrée pour
accéder aux objets persistants
\item
Les \texttt{querysets}, qui permettent de filtrer des ensembles ou
sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour
ajouter d'autres filtres à des filtres existants, et fonctionnent
comme un super jeu d'abstraction pour accéder à nos données
(persistentes).
\end{itemize}
Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut \texttt{objects}, qui correspond à un manager (ou un gestionnaire, si vous préférez).
Ce gestionnaire constitue l'interface par laquelle vous accéderez à la base de données.
Mais pour cela, vous aurez aussi besoin d'appliquer certains requêtes ou filtres.
Et pour cela, vous aurez besoin des \texttt{querysets}, qui consistent en des ensembles de requêtes.
Si on veut connaître la requête SQL sous-jacente à l'exécution du queryset, il suffit d'appeler la fonction str() sur la propriété \texttt{query}:
\begin{verbatim}
queryset = Wishlist.objects.all()
print(queryset.query)
\end{verbatim}
Chaque définition de modèle utilise un \texttt{Manager}, afin d'accéder
à la base de données et traiter nos demandes. Indirectement, une
instance de modèle ne \textbf{connait} \textbf{pas} la base de données:
c'est son gestionnaire qui a cette tâche. Il existe deux exceptions à
cette règle: les méthodes \texttt{save()} et \texttt{update()}.
\begin{itemize}
\item
Instanciation: MyClass()
\item
Récupération: MyClass.objects.get(pk=\ldots\hspace{0pt})
\item
Sauvegarde : MyClass().save()
\item
Création: MyClass.objects.create(\ldots\hspace{0pt})
\item
Liste des enregistrements: MyClass.objects.all()
\end{itemize}
Par défaut, le gestionnaire est accessible au travers de la propriété
\texttt{objects}. Cette propriété a une double utilité:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Elle est facile à surcharger - il nous suffit de définir une nouvelle
classe héritant de ModelManager, puis de définir, au niveau de la
classe, une nouvelle assignation à la propriété \texttt{objects}
\item
Il est tout aussi facile de définir d'autres propriétés présentant des
filtres bien spécifiques.
\end{enumerate}
\section{Conclusions}
Le modèle proposé par Django est un composant extrêmement performant, mais fort couplé avec le coeur du framework.
Si tous les composants peuvent être échangés avec quelques manipulations, le cas du modèle sera
plus difficile à interchanger.
A côté de cela, il permet énormément de choses, et vous fera gagner un temps précieux, tant en rapidité d'essais/erreurs, que de preuves de concept.
Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (\textbf{ForeignKey}) d'une classe A vers une classe B.
Pour représenter d'autres types de relations, il existe également les champs de type \textbf{ManyToManyField}, afin de représenter une relation N-N.
Il existe également un type de champ spécial pour les clés étrangères, qui est le Les champs de type \textbf{OneToOneField}, pour représenter une relation 1-1.

View File

@ -4,18 +4,498 @@
Django fonctionne sur un
\href{https://docs.djangoproject.com/en/dev/internals/release-process/}{roulement de trois versions mineures pour une version majeure}, clôturé par une version LTS (\emph{Long Term Support}).
\section{Gestion des dépendances}
Comme nous en avons déjà discuté, PIP est la solution que nous avons choisie pour la gestion de nos dépendances.
Pour installer une nouvelle librairie, vous pouvez simplement passer par la commande \texttt{pip\ install\ \textless{}my\_awesome\_library\textgreater{}}.
Dans le cas de Django, et après avoir activé l'environnement, nous pouvons à présent y installer Django. Comme expliqué ci-dessus, la librairie restera indépendante du reste du système, et ne polluera aucun autre projet. nous exécuterons donc la commande suivante:
\begin{verbatim}
$ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift-
env/Scrips/activate.bat pour Windows.
$ pip install django
Collecting django
Downloading Django-3.1.4
100% |################################|
Installing collected packages: django
Successfully installed django-3.1.4
\end{verbatim}
Ici, la commande \texttt{pip\ install\ django} récupère la \textbf{dernière version connue disponible dans les dépôts \url{https://pypi.org/}} (sauf si vous en avez définis d'autres. Mais c'est hors sujet).
Nous en avons déjà discuté: il est important de bien spécifier la version que vous souhaitez utiliser, sans quoi vous risquez de rencontrer des effets de bord.
L'installation de Django a ajouté un nouvel exécutable: \texttt{django-admin}, que l'on peut utiliser pour créer notre nouvel espace de travail. Par la suite, nous utiliserons \texttt{manage.py}, qui constitue un \textbf{wrapper} autour de \texttt{django-admin}.
Pour démarrer notre projet, nous lançons
\texttt{django-admin\ startproject\ gwift}:
\begin{verbatim}
$ django-admin startproject gwift
\end{verbatim}
Cette action a pour effet de créer un nouveau dossier \texttt{gwift},
dans lequel nous trouvons la structure suivante:
\begin{verbatim}
$ tree gwift
gwift
-- gwift
----- asgi.py
----- __init__.py
----- settings.py
----- urls.py
----- wsgi.py
-- manage.py
\end{verbatim}
C'est dans ce répertoire que vont vivre tous les fichiers liés au
projet. Le but est de faire en sorte que toutes les opérations
(maintenance, déploiement, écriture, tests, \ldots\hspace{0pt}) puissent
se faire à partir d'un seul point d'entrée.
L'utilité de ces fichiers est définie ci-dessous:
\begin{itemize}
\item
\texttt{settings.py} contient tous les paramètres globaux à notre
projet.
\item
\texttt{urls.py} contient les variables de routes, les adresses
utilisées et les fonctions vers lesquelles elles pointent.
\item
\texttt{manage.py}, pour toutes les commandes de gestion.
\item
\texttt{asgi.py} contient la définition de l'interface
\href{https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface}{ASGI},
le protocole pour la passerelle asynchrone entre votre application et
le serveur Web.
\item
\texttt{wsgi.py} contient la définition de l'interface
\href{https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface}{WSGI},
qui permettra à votre serveur Web (Nginx, Apache, \ldots\hspace{0pt})
de faire un pont vers votre projet.
\end{itemize}
Indiquer qu'il est possible d'avoir plusieurs structures de dossiers et
qu'il n'y a pas de "magie" derrière toutes ces commandes.
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel
nous stockerons les dépendances et un fichier README:
TODO
Comme nous venons d'ajouter une dépendance à notre projet, profitons-en pour créer un fichier reprenant tous les dépendances de notre projet.
Celles-ci sont normalement placées dans un fichier \texttt{requirements.txt}.
Dans un premier temps, ce fichier peut être placé directement à la racine du projet, mais on préférera rapidement le déplacer dans un sous-répertoire spécifique (\texttt{requirements}), afin de grouper les dépendances en fonction de leur environnement de destination:
\begin{itemize}
\item
\texttt{base.txt}
\item
\texttt{dev.txt}
\item
\texttt{production.txt}
\end{itemize}
Au début de chaque fichier, il suffit d'ajouter la ligne \texttt{-r\ base.txt}, puis de lancer l'installation grâce à un \texttt{pip\ install\ -r\ \textless{}nom\ du\ fichier\textgreater{}}.
De cette manière, il est tout à fait acceptable de n'installer \texttt{flake8} et \texttt{django-debug-toolbar} qu'en développement par exemple.
Dans l'immédiat, nous allons ajouter \texttt{django} dans une version égale à la version 3.2 dans le fichier \texttt{requirements/base.txt}.
\begin{verbatim}
$ echo 'django==3.2' > requirements/base.txt
$ echo '-r base.txt' > requirements/prod.txt
$ echo '-r base.txt' > requirements/dev.txt
\end{verbatim}
Une bonne pratique consiste à également placer un fichier \texttt{requirements.txt} à la racine du projet, et dans lequel nous retrouverons le contenu \texttt{-r requirements/production.txt} (notamment pour Heroku).
Prenez directement l'habitude de spécifier la version ou les versions compatibles: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets.
Pour être sûr et certain le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances.
Entre deux versions d'une même librairie, des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc.
Il suffit de parcourirles pages de \emph{Changements incompatibles avec les anciennes versions dans Django}
\href{https://docs.djangoproject.com/fr/3.1/releases/3.0/}{(par exemple ici pour le passage de la 3.0 à la 3.1)} pour réaliser que certaines opérations ne sont pas anodines, et que sans filet de sécurité, c'est le
mur assuré.
Avec les mécanismes d'intégration continue et de tests unitaires, nous verrons plus loin comment se prémunir d'un changement inattendu.
\includegraphics{images/django-support-lts.png}
La version utilisée sera une bonne indication à prendre en considération pour nos dépendances, puisqu'en visant une version particulière, nous ne devrons pratiquement pas nous soucier (bon, un peu quand même, mais nous le verrons plus tard\ldots\hspace{0pt}) des dépendances à installer, pour peu que l'on reste sous un certain seuil.
Dans les étapes ci-dessous, nous épinglerons une version LTS afin de nous assurer une certaine sérénité d'esprit (= dont nous ne occuperons pas pendant les 3 prochaines années).
\section{Django}
Comme nous l'avons vu ci-dessus, \texttt{django-admin} permet de créer un nouveau projet.
Nous faisons ici une distinction entre un \textbf{projet} et une \textbf{application}:
\begin{itemize}
\item
\textbf{Un projet} représente l'ensemble des applications, paramètres,
pages HTML, middlewares, dépendances, etc., qui font que votre code
fait ce qu'il est sensé faire.
\item
\textbf{Une application} est un contexte d'exécution, idéalement
autonome, d'une partie du projet.
\end{itemize}
Pour \texttt{gwift}, nous aurons:
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-gwift.png}
\caption{Django Projet vs Applications}
\end{figure}
\begin{enumerate}
\item
Une première application pour la gestion des listes de souhaits et des
éléments,
\item
Une deuxième application pour la gestion des utilisateurs,
\item
Voire une troisième application qui gérera les partages entre
utilisateurs et listes.
\end{enumerate}
Nous voyons également que la gestion des listes de souhaits et éléments aura besoin de la gestion des utilisateurs - elle n'est pas autonome -, tandis que la gestion des utilisateurs n'a aucune autre dépendance
qu'elle-même.
Pour \texttt{khana}, nous pourrions avoir quelque chose comme ceci:
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-khana.png}
\caption{Django Project vs Applications}
\end{figure}
En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la gestion des utilisateurs et la possibilité qu'ils auront de communiquer entre eux.
Ceci pourrait être commun aux deux applications.
Nous pouvons clairement visualiser le principe de \textbf{contexte} pour une application: celle-ci viendra avec son modèle, ses tests, ses vues et son paramétrage et pourrait ainsi être réutilisée dans un autre projet.
C'est en ça que consistent les \href{https://www.djangopackages.com/}{paquets Django} déjà disponibles:
ce sont "\emph{simplement}" de petites applications empaquetées et pouvant être réutilisées dans différents contextes (eg. \href{https://github.com/tomchristie/django-rest-framework}{Django-Rest-Framework}, \href{https://github.com/django-debug-toolbar/django-debug-toolbar}{Django-Debug-Toolbar}, ...
\subsection{manage.py}
Le fichier \texttt{manage.py} que vous trouvez à la racine de votre projet est un \textbf{wrapper} sur les commandes \texttt{django-admin}.
A partir de maintenant, nous n'utiliserons plus que celui-là pour tout
ce qui touchera à la gestion de notre projet:
\begin{itemize}
\item
\texttt{manage.py\ check} pour vérifier (en surface\ldots\hspace{0pt})
que votre projet ne rencontre aucune erreur évidente
\item
\texttt{manage.py\ check\ -\/-deploy}, pour vérifier (en surface
aussi) que l'application est prête pour un déploiement
\item
\texttt{manage.py\ runserver} pour lancer un serveur de développement
\item
\texttt{manage.py\ test} pour découvrir les tests unitaires
disponibles et les lancer.
\end{itemize}
La liste complète peut être affichée avec \texttt{manage.py\ help}. Vous
remarquerez que ces commandes sont groupées selon différentes
catégories:
\begin{itemize}
\item
\textbf{auth}: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
\item
\textbf{django}: vérifier la \textbf{compliance} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, ...
\item
\textbf{sessions}: suppressions des sessions en cours
\item
\textbf{staticfiles}: gestion des fichiers statiques et lancement du serveur de développement.
\end{itemize}
Nous verrons plus tard comment ajouter de nouvelles commandes.
Si nous démarrons la commande \texttt{python\ manage.py\ runserver},
nous verrons la sortie console suivante:
\begin{verbatim}
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
[...]
December 15, 2020 - 20:45:07
Django version 3.1.4, using settings 'gwift.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
\end{verbatim}
Si nous nous rendons sur la page \url{http://127.0.0.1:8000} (ou \url{http://localhost:8000}) comme le propose si gentiment notre (nouveau) meilleur ami, nous verrons ceci:
\begin{figure}
\centering
\includegraphics{images/django/manage-runserver.png}
\caption{python manage.py runserver (Non, ce n'est pas Challenger)}
\end{figure}
Nous avons mis un morceau de la sortie console entre crochet \texttt{{[}\ldots{}\hspace{0pt}{]}} ci-dessus, car elle concerne les migrations.
Si vous avez suivi les étapes jusqu'ici, vous avez également dû voir un message type \texttt{You\ have\ 18\ unapplied\ migration(s).\ {[}\ldots{}\hspace{0pt}{]}\ Run\ \textquotesingle{}python\ manage.py\ migrate\textquotesingle{}\ to\ apply\ them.}
Cela concerne les migrations, et c'est un point que nous verrons un peu plus tard.
\section{Nouvelle application}
Maintenant que nous avons a vu à quoi servait \texttt{manage.py}, nous pouvons créer notre nouvelle application grâce à la commande \texttt{manage.py\ startapp\ \textless{}label\textgreater{}}.
Notre première application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir.
De manière générale, essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc \texttt{wish}.
C'est parti pour \texttt{manage.py\ startapp\ wish}!
\begin{verbatim}
$ python manage.py startapp wish
\end{verbatim}
Résultat? Django nous a créé un répertoire \texttt{wish}, dans lequel nous trouvons les fichiers et dossiers suivants:
\begin{itemize}
\item
\texttt{wish/init.py} pour que notre répertoire \texttt{wish} soit converti en package Python.
\item
\texttt{wish/admin.py} servira à structurer l'administration de notre application.
Chaque information peut être administrée facilement au travers d'une interface générée à la volée par le framework.
Nous y reviendrons par la suite.
\item
\texttt{wish/apps.py} qui contient la configuration de l'application et qui permet notamment de fixer un nom ou un libellé \url{https://docs.djangoproject.com/en/stable/ref/applications/}
\item
\texttt{wish/migrations/} est le dossier dans lequel seront stockées toutes les différentes migrations de notre application (= toutes les modifications que nous apporterons aux données que nous souhaiterons manipuler)
\item
\texttt{wish/models.py} représentera et structurera nos données, et est intimement lié aux migrations.
\item
\texttt{wish/tests.py} pour les tests unitaires.
\end{itemize}
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire \texttt{wish} dans votre répertoire \texttt{gwift} existant.
C'est une forme de convention.
La structure de vos répertoires devient celle-ci:
TODO
Notre application a bien été créée, et nous l'avons déplacée dans le répertoire \texttt{gwift} !
\section{Fonctionnement général}
Le métier de programmeur est devenu de plus en plus complexe.
Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actios à réaliser: requêtes en bases de données, construction de la page, ...
La recherche d'une solution a un problème n'était pas spécialement plus complexe - dans la mesure où le rendu des enregistrements en direct n'était finalement qu'une forme un chouia plus évoluée du \texttt{print()} ou des \texttt{System.out.println()} - mais c'était l'évolutivité des applications qui en prenait un coup: une grosse partie des tâches étaient dupliquées entre les différentes pages, et l'ajout d'une nouvelle fonctionnalité était relativement ardue.
Django (et d'autres cadriciels) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ repeat\ yourself} \footnote{DRY}.
Chaque morceau de code ne doit apparaitre qu'une seule fois, afin de limiter au maximum la redite (et donc, l'application d'un même correctif à différents endroits).
Le chemin parcouru par une requête est expliqué en (petits) détails ci-dessous.
\begin{figure}
\centering
\includegraphics{images/diagrams/django-how-it-works.png}
\caption{How it works}
\end{figure}
\textbf{1. Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application}.
Ici, nous prenons l'exemple de l'URL fictive \texttt{https://gwift/wishes/91827}.
Lorsque cette URL "arrive" dans notre application, son point d'entrée se trouvera au niveau des fichiers \texttt{asgi.py} ou \texttt{wsgi.py}.
Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu'elle va parcourir.
\textbf{Etape 0} - La première étape consiste à vérifier que cette URL répond à un schéma que nous avons défini dans le fichier \texttt{gwift/urls.py}.
\textbf{Etape 1} - Si ce n'est pas le cas, l'application n'ira pas plus loin et retournera une erreur à l'utilisateur.
\textbf{Etape 2} - Django va parcourir l'ensemble des \emph{patterns} présents dans le fichier \texttt{urls.py} et s'arrêtera sur le premier qui correspondra à la requête qu'il a reçue.
Ce cas est relativement trivial: la requête \texttt{/wishes/91827} a une correspondance au
niveau de la ligne \texttt{path("wishes/\textless{}int:wish\_id\textgreater{}} dans l'exemple ci-dessous. Django va alors appeler la fonction \footnote{Qui ne sera pas toujours une fonction. Django s'attend à trouver un \emph{callable}, c'est-à-dire n'importe quel élément qu'il peut appeler comme une fonction.} associée à ce \emph{pattern}, c'est-à-dire \texttt{wish\_details} du module \texttt{gwift.views}.
\begin{minted}{Python}
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details),
]
\end{minted}
\begin{itemize}
\item
Nous importons la fonction \texttt{wish\_details} du module
\texttt{gwift.views}
\item
Champomy et cotillons! Nous avons une correspondance avec
\texttt{wishes/details/91827}
\end{itemize}
TODO: En fait, il faudrait quand même s'occuper du modèle ici.
TODO: et de la mise en place de l'administration, parce que nous en aurons besoin pour les étapes de déploiement.
Nous n'allons pas nous occuper de l'accès à la base de données pour le moment (nous nous en occuperons dans un prochain chapitre) et nous nous contenterons de remplir un canevas avec un ensemble de données.
Le module \texttt{gwift.views} qui se trouve dans le fichier \texttt{gwift/views.py} peut ressembler à ceci:
\begin{minted}{Python}
[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"now": datetime.now()
}
return render(
request,
"wish_details.html",
context
)
\end{minted}
Pour résumer, cette fonction permet:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
De construire un \emph{contexte}, qui est représenté sous la forme d'un dictionnaire associant des clés à des valeurs. Les clés sont respectivement \texttt{user\_name}, \texttt{user\_first\_name} et \texttt{now}, tandis que leurs valeurs respectives sont \texttt{Bond}, \texttt{James} et le \texttt{moment\ présent} \footnote{Non, pas celui d'Eckhart Tolle}.
\item
Nous passons ensuite ce dictionnaire à un canevas, \texttt{wish\_details.html}
\item
L'application du contexte sur le canevas nous donne un résultat.
\end{enumerate}
\begin{minted}{html}
<!-- fichier wish_details.html -->
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.</p>
<p>This page was generated at {{ now }}</p>
</body>
</html>
\end{minted}
Après application de notre contexte sur ce template, nous obtiendrons ce document, qui sera renvoyé au navigateur de l'utilisateur qui aura fait la requête initiale:
\begin{minted}{html}
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is Bond. James Bond.</p>
<p>This page was generated at 2027-03-19 19:47:38</p>
</body>
</html>
\end{minted}
\begin{figure}
\centering
\includegraphics{images/django/django-first-template.png}
\caption{Résultat}
\end{figure}
\section{Configuration globale}
\subsection{setup.cfg}
→ Faire le lien avec les settings → Faire le lien avec les douze
facteurs → Construction du fichier setup.cfg
\begin{verbatim}
[flake8]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[pycodestyle]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[mypy]
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
plugins = mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = config.settings.test
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True
[coverage:run]
include = khana/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin
\end{verbatim}
\subsection{Structure finale}
Nous avons donc la structure finale pour notre environnement de travail:
\subsection{Cookie-cutter}
Pfiou! Ca en fait des commandes et du boulot pour "juste" démarrer un nouveau projet, non? Sachant qu'en plus, nous avons dû modifier des fichiers, déplacer des dossiers, ajouter des dépendances, configurer une
base de données, ...
Bonne nouvelle! Il existe des générateurs, permettant de démarrer rapidement un nouveau projet sans (trop) se prendre la tête.
Le plus connu (et le plus personnalisable) est \href{https://cookiecutter.readthedocs.io/}{Cookie-Cutter}, qui se base sur des canevas \emph{type \href{https://pypi.org/project/Jinja2/}{Jinja2}}, pour créer une
arborescence de dossiers et fichiers conformes à votre manière de travailler.
Et si vous avez la flemme de créer votre propre canevas, vous pouvez utiliser \href{https://cookiecutter-django.readthedocs.io}{ceux qui existent déjà}.
Pour démarrer, créez un environnement virtuel (comme d'habitude):
\begin{verbatim}
python -m venv .venvs\cookie-cutter-khana
.venvs\cookie-cutter-khana\Scripts\activate.bat
(cookie-cutter-khana) $ pip install cookiecutter
Collecting cookiecutter
[...]
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 arrow-0.17.0
binaryornot-0.4.4 certifi-2020.12.5 chardet-4.0.0 click-7.1.2 cookiecutter-
1.7.2 idna-2.10 jinja2-time-0.2.0 poyo-0.5.0 python-dateutil-2.8.1 python-
slugify-4.0.1 requests-2.25.1 six-1.15.0 text-unidecode-1.3 urllib3-1.26.2
(cookie-cutter-khana) $ cookiecutter https://github.com/pydanny/cookiecutter-
django
[...]
[SUCCESS]: Project initialized, keep up the good work!
\end{verbatim}
Si vous explorez les différents fichiers, vous trouverez beaucoup de similitudes avec la configuration que nous vous proposions ci-dessus.
En fonction de votre expérience, vous serez tenté de modifier certains paramètres, pour faire correspondre ces sources avec votre utilisation ou vos habitudes.
Il est aussi possible d'utiliser l'argument \texttt{-\/-template}, suivie d'un argument reprenant le nom de votre projet (\texttt{\textless{}my\_project\textgreater{}}), lors de l'initialisation d'un projet avec la commande \texttt{startproject} de \texttt{django-admin}, afin de calquer votre arborescence sur un projet
existant.
La \href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startproject}{documentation} à ce sujet est assez complète.
\begin{verbatim}
django-admin.py startproject --template=https://[...].zip <my_project>
\end{verbatim}
\section{Tests unitaires}
Chaque application est créée par défaut avec un fichier \textbf{tests.py}, qui inclut la classe \texttt{TestCase} depuis le package \texttt{django.test}:
On a deux choix ici:
\begin{enumerate}
@ -91,3 +571,42 @@ La configuration peut se faire dans un fichier .coveragerc que vous placerez à
$ coverage html
\end{verbatim}
\subsection{Réalisation des tests}
En résumé, il est recommandé de:
\begin{enumerate}
\item
Tester que le nommage d'une URL (son attribut \texttt{name} dans les fichiers \texttt{urls.py}) corresponde à la fonction que l'on y a définie
\item
Tester que l'URL envoie bien vers l'exécution d'une fonction (et que cette fonction est celle que l'on attend)
\end{enumerate}
\subsubsection{Tests de nommage}
\begin{minted}{python}
from django.core.urlresolvers import reverse
from django.test import TestCase
class HomeTests(TestCase):
def test_home_view_status_code(self):
url = reverse("home")
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
\end{minted}
\subsubsection{Tests d'URLs}
\begin{minted}{python}
from django.core.urlresolvers import reverse
from django.test import TestCase
from .views import home
class HomeTests(TestCase):
def test_home_view_status_code(self):
view = resolve("/")
self.assertEquals(view.func, home)
\end{minted}

View File

@ -657,6 +657,12 @@ Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer p
Ajoutez-le dans le fichier \texttt{requirements/base.txt}, et lancez une couverture de code grâce à la commande \texttt{coverage}.
La configuration peut se faire dans un fichier \texttt{.coveragerc} que vous placerez à la racine de votre projet, et qui sera lu lors de l'exécution.
\section{Gestion des versions de l'interpréteur}
\begin{verbatim}
pyenv install 3.10
\end{verbatim}
\section{Matrice de compatibilité}
L'intérêt de la matrice de compatibilité consiste à spécifier un ensemble de plusieurs versions d'un même interpréteur (ici, Python), afin de s'assurer que votre application continue à fonctionner.

View File

@ -109,8 +109,33 @@ Une solution serait de passer par un dictionnaire, de façon à ramener la compl
\caption{La même version, avec une complexité réduite à 1}
\end{listing}
\section{Types de tests}
\section{Tests unitaires}
De manière générale, si nous nous rendons compte que les tests sont trop compliqués à écrire ou nous coûtent trop de temps, cest sans doute que larchitecture de la solution nest pas adaptée et que les composants sont couplés les uns aux autres.
Dans ces cas, il sera nécessaire de refactoriser le code, afin que chaque module puisse être testé
indépendamment des autres. \cite{clean_code}
Le plus important est de toujours corréler les phases de tests indépendantes du reste du travail (de développement, ici), en lautomatisant au plus près de sa source de création:
\begin{quote}
Martin Fowler observes that, in general, "a ten minute build [and test process] is perfectly within reason...
[We first] do the compilation and run tests that are more localized unit tests with the database completely stubbed out. Such tests can run very fast, keeping within the ten minutes guideline.
However any bugs that involve larger scale intercations, particularly those involving the real database, wont be found.
The second stage build runs a different suite of tests [acceptance tests] that do hit the real database and involve more end-to-end behavior.
This suite may take a couple of hours to run.
-- Robert C. Martin, Clean Architecture
\end{quote}
\subsection{Tests unitaires}
\begin{quote}
The aim of a unit test is to show that a single part of the application
does what programmer intends it to.
\end{quote}
Les tests unitaires ciblent typiquement une seule fonction, classe ou méthode, de manière isolée, en fournissant au développeur lassurance que son code réalise ce quil en attend.
Pour plusieurs raisons (et notamment en raison de performances), les tests unitaires utilisent souvent des données stubbées - pour éviter dappeler le "vrai" service
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc.
Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil.
@ -129,6 +154,19 @@ vérifier que le code est \textbf{bien} testé, mais juste de vérifier
\textbf{quelle partie} du code est testée. Le paquet \texttt{coverage}
se charge d'évaluer le pourcentage de code couvert par les tests.
\subsection{Tests d'acceptance}
\section{Tests d'intégration}
\begin{quote}
The objective of acceptance tests is to prove that our application does
what the customer meant it to.
\end{quote}
Les tests dacceptance vérifient que lapplication fonctionne comme convenu, mais à un
plus haut niveau (fonctionnement correct dune API, validation dune chaîne dactions
effectuées par un humain, ...).
\subsection{Tests d'intégration}
Les tests dintégration vérifient que lapplication coopère correctement avec les systèmes
périphériques

View File

@ -76,7 +76,236 @@ TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à
test_django_gecko.py
2 directories, 5 files
\end{verbatim}
Ceci signifie que nous avons directement (et de manière standard):
\begin{itemize}
\item
Un répertoire django-gecko, qui porte le nom de l'application que vous
venez de créer
\item
Un répertoires tests, libellé selon les standards de pytest
\item
Un fichier README.rst (qui ne contient encore rien)
\item
Un fichier pyproject.toml, qui contient ceci:
\end{itemize}
\begin{verbatim}
[tool.poetry]
name = "django-gecko"
version = "0.1.0"
description = ""
authors = ["... <...@grimbox.be>"]
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
\end{verbatim}
La commande \texttt{poetry\ init} permet de générer interactivement les
fichiers nécessaires à son intégration dans un projet existant.
J'ai pour habitude de conserver mes projets dans un répertoire
\texttt{\textasciitilde{}/Sources/} et mes environnements virtuels dans
un répertoire \texttt{\textasciitilde{}/.venvs/}.
Cette séparation évite que l'environnement virtuel ne se trouve dans le
même répertoire que les sources, ou ne soit accidentellement envoyé vers
le système de gestion de versions. Elle évite également de rendre ce
répertoire "visible" - il ne s'agit au fond que d'un paramètre de
configuration lié uniquement à votre environnement de développement; les
environnements virtuels étant disposables, il n'est pas conseillé de
trop les lier au projet qui l'utilise comme base. Dans la suite de ce
chapitre, je considérerai ces mêmes répertoires, mais n'hésitez pas à
les modifier.
DANGER: Indépendamment de l'endroit où vous stockerez le répertoire
contenant cet environnement, il est primordial de \textbf{ne pas le
conserver dans votre dépôt de stockager}. Cela irait à l'encontre des
douze facteurs, cela polluera inutilement vos sources et créera des
conflits avec l'environnement des personnes qui souhaiteraient
intervenir sur le projet.
Pur créer notre répertoire de travail et notre environnement virtuel,
exécutez les commandes suivantes:
\begin{verbatim}
mkdir ~/.venvs/
python -m venv ~/.venvs/gwift-venv
\end{verbatim}
Ceci aura pour effet de créer un nouveau répertoire
(\texttt{\textasciitilde{}/.venvs/gwift-env/}), dans lequel vous
trouverez une installation complète de l'interpréteur Python. Votre
environnement virtuel est prêt, il n'y a plus qu'à indiquer que nous
souhaitons l'utiliser, grâce à l'une des commandes suivantes:
\begin{verbatim}
# GNU/Linux, macOS
source ~/.venvs/gwift-venv/bin/activate
# MS Windows, avec Cmder
~/.venvs/gwift-venv/Scripts/activate.bat
# Pour les deux
(gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$
\end{verbatim}
A présent que l'environnement est activé, tous les binaires de cet
environnement prendront le pas sur les binaires du système. De la même
manière, une variable \texttt{PATH} propre est définie et utilisée, afin
que les librairies Python y soient stockées. C'est donc dans cet
environnement virtuel que nous retrouverons le code source de Django,
ainsi que des librairies externes pour Python une fois que nous les
aurons installées.
Pour les curieux, un environnement virtuel n'est jamais qu'un répertoire
dans lequel se trouve une installation fraîche de l'interpréteur, vers
laquelle pointe les liens symboliques des binaires. Si vous recherchez
l'emplacement de l'interpréteur avec la commande \texttt{which\ python},
vous recevrez comme réponse
\texttt{/home/fred/.venvs/gwift-env/bin/python}.
Pour sortir de l'environnement virtuel, exécutez la commande
\texttt{deactivate}. Si vous pensez ne plus en avoir besoin, supprimer
le dossier. Si nécessaire, il suffira d'en créer un nouveau.
Pour gérer des versions différentes d'une même librairie, il nous suffit
de jongler avec autant d'environnements que nécessaires. Une application
nécessite une version de Django inférieure à la 2.0 ? On crée un
environnement, on l'active et on installe ce qu'il faut.
Cette technique fonctionnera autant pour un poste de développement que
sur les serveurs destinés à recevoir notre application.
Par la suite, nous considérerons que l'environnement virtuel est
toujours activé, même si \texttt{gwift-env} n'est pas indiqué.
a manière recommandée pour la gestion des dépendances consiste à les
épingler dans un fichier requirements.txt, placé à la racine du projet.
Ce fichier reprend, ligne par ligne, chaque dépendance et la version
nécessaire. Cet épinglage est cependant relativement basique, dans la
mesure où les opérateurs disponibles sont ==, et \textgreater=.
Poetry propose un épinglage basé sur SemVer. Les contraintes qui peuvent
être appliquées aux dépendances sont plus touffues que ce que proposent
pip -r, avec la présence du curseur \^{}, qui ne modifiera pas le nombre
différent de zéro le plus à gauche:
\begin{verbatim}
^1.2.3 (où le nombre en question est 1) pourra proposer une mise à jour jusqu'à la version juste avant la version 2.0.0
^0.2.3 pourra être mise à jour jusqu'à la version juste avant 0.3.0.
...
\end{verbatim}
L'avantage est donc que l'on spécifie une version majeure - mineure - patchée, et que l'on pourra spécifier accepter toute mise à jour jusqu'à la prochaine version majeure - mineure patchée (non incluse).
Une bonne pratique consiste également, tout comme pour npm, à intégrer le fichier de lock (poetry.lock) dans le dépôt de sources: de cette manière, seules les dépendances testées (et intégrées) seront considérées sur tous les environnements de déploiement.
Il est alors nécessaire de passer par une action manuelle (poetry update) pour mettre à jour le fichier de verrou, et assurer une mise à jour en sécurité (seules les dépendances testées sont prises en compte)
et de qualité (tous les environnements utilisent la même version d'une dépendance).
L'ajout d'une nouvelle dépendance à un projet se réalise grâce à la commande \texttt{poetry\ add\ \textless{}dep\textgreater{}}:
\begin{verbatim}
$ poetry add django
Using version ^3.2.3 for Django
Updating dependencies
Resolving dependencies... (5.1s)
Writing lock file
Package operations: 8 installs, 1 update, 0 removals
• Installing pyparsing (2.4.7)
• Installing attrs (21.2.0)
• Installing more-itertools (8.8.0)
• Installing packaging (20.9)
• Installing pluggy (0.13.1)
• Installing py (1.10.0)
• Installing wcwidth (0.2.5)
• Updating django (3.2 -> 3.2.3)
• Installing pytest (5.4.3)
\end{verbatim}
Elle est ensuite ajoutée à notre fichier \texttt{pyproject.toml}:
\begin{verbatim}
[...]
[tool.poetry.dependencies]
python = "^3.9"
Django = "^3.2.3"
[...]
\end{verbatim}
Et contrairement à \texttt{pip}, pas besoin de savoir s'il faut pointer vers un fichier (\texttt{-r}) ou un dépôt VCS (\texttt{-e}), puisque Poetry va tout essayer, {[}dans un certain ordre{]}(\url{https://python-poetry.org/docs/cli/\#add}).
L'avantage également (et cela m'arrive encore souvent, ce qui fait hurler le runner de Gitlab), c'est qu'il n'est plus nécessaire de penser à épingler la dépendance que l'on vient d'installer parmi les fichiers de
requirements, puisqu'elles s'y ajoutent automatiquement grâce à la commande \texttt{add}.
\subsubsection{Python Packaging Made Easy}
Cette partie dépasse mes compétences et connaissances, dans la mesure où je n'ai jamais rien packagé ni publié sur {[}pypi.org{]}(pypi.org).
Ce n'est pas l'envie qui manque, mais les idées et la nécessité.
Ceci dit, Poetry propose un ensemble de règles et une préconfiguration qui (doivent) énormément facilite(r) la mise à disposition de librairies sur Pypi - et rien que ça, devrait ouvrir une partie de l'écosystème.
Les chapitres 7 et 8 de {[}Expert Python Programming - Third Edtion{]}(\#), écrit par Michal Jaworski et Tarek Ziadé en parlent très bien:
\begin{quote}
Python packaging can be a bit overwhelming at first. The main reason for
that is the confusion about proper tools for creating Python packages.
Anyway, once you create your first package, you will se that this is as
hard as it looks. Also, knowing propre, state-of-the-art packaging helps
a lot.
\end{quote}
En gros, c'est ardu-au-début-mais-plus-trop-après.
Et c'est heureusement suivi et documenté par la PyPA (\textbf{\href{https://github.com/pypa}{Python Packaging Authority}}).
Les étapes sont les suivantes:
\begin{enumerate}
\item
Utiliser setuptools pour définir les projets et créer les distributions sources,
\item
Utiliser \textbf{wheels} pour créer les paquets,
\item
Passer par \textbf{twine} pour envoyer ces paquets vers PyPI
\item
Définir un ensemble d'actions (voire, de plugins nécessaires - lien avec le VCS, etc.) dans le fichier \texttt{setup.py}, et définir les propriétés du projet ou de la librairie dans le fichier \texttt{setup.cfg}.
\end{enumerate}
Avec Poetry, deux commandes suffisent (théoriquement - puisque je n'ai pas essayé): \texttt{poetry\ build} et \texttt{poetry\ publish}:
\begin{verbatim}
$ poetry build
Building geco (0.1.0)
- Building sdist
- Built geco-0.1.0.tar.gz
- Building wheel
- Built geco-0.1.0-py3-none-any.whl
$ tree dist/
dist/
-- geco-0.1.0-py3-none-any.whl
-- geco-0.1.0.tar.gz
0 directories, 2 files
\end{verbatim}
Ce qui est quand même 'achement plus simple que d'appréhender tout un
écosystème.
\section{Un système de virtualisation}
Par "\emph{système de virtualisation}", nous entendons n'importe quel application, système d'exploitation, système de containeurisation, ... qui permette de créer ou recréer un environnement de

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,5 +1,6 @@
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments pdflatex main.tex -shell-escape
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments makeindex main.idx
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments bibtex main
# docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments makeglossaries main
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments pdflatex main.tex -shell-escape

55
glossary.tex Normal file
View File

@ -0,0 +1,55 @@
\chapter{Glossaire}
\begin{description}
\item[http]
\emph{HyperText Transfer Protocol}, ou plus généralement le protocole
utilisé (et détourné) pour tout ce qui touche au \textbf{World Wide
Web}. Il existe beaucoup d'autres protocoles d'échange de données, comme
\href{https://fr.wikipedia.org/wiki/Gopher}{Gopher},
\href{https://fr.wikipedia.org/wiki/File_Transfer_Protocol}{FTP} ou
\href{https://fr.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol}{SMTP}.
\item[IaaS]
\emph{Infrastructure as a Service}, où un tiers vous fournit des
machines (généralement virtuelles) que vous devrez ensuite gérer en bon
père de famille. L'IaaS propose souvent une API, qui vous permet
d'intégrer la durée de vie de chaque machine dans vos flux - en créant,
augmentant, détruisant une machine lorsque cela s'avère nécessaire.
\item[MVC]
Le modèle \emph{Model-View-Controler} est un patron de conception
autorisant un faible couplage entre la gestion des données (le
\emph{Modèle}), l'affichage et le traitement de celles (la \emph{Vue})
et la glue entre ces deux composants (au travers du \emph{Contrôleur}).
\href{https://en.wikipedia.org/wiki/Model\%E2\%80\%93view\%E2\%80\%93controller}{Wikipédia}
\item[ORM]
\emph{Object Relational Mapper}, où une instance est directement (ou à
proximité) liée à un mode de persistance de données.
\item[PaaS]
\emph{Platform as a Service}, qui consiste à proposer les composants
d'une plateforme (Redis, PostgreSQL, \ldots\hspace{0pt}) en libre
service et disponibles à la demande (quoiqu'après avoir communiqué son
numéro de carte de crédit\ldots\hspace{0pt}).
\item[POO]
La \emph{Programmation Orientée Objet} est un paradigme de programmation
informatique. Elle consiste en la définition et l'interaction de briques
logicielles appelées objets ; un objet représente un concept, une idée
ou toute entité du monde physique, comme une voiture, une personne ou
encore une page d'un livre. Il possède une structure interne et un
comportement, et il sait interagir avec ses pairs. Il s'agit donc de
représenter ces objets et leurs relations ; l'interaction entre les
objets via leurs relations permet de concevoir et réaliser les
fonctionnalités attendues, de mieux résoudre le ou les problèmes. Dès
lors, l'étape de modélisation revêt une importance majeure et nécessaire
pour la POO. C'est elle qui permet de transcrire les éléments du réel
sous forme virtuelle.
\href{https://fr.wikipedia.org/wiki/Programmation_orient\%C3\%A9e_objet}{Wikipédia}
\item[S3]
Amazon \emph{Simple Storage Service} consiste en un système
d'hébergement de fichiers, quels qu'ils soient. Il peut s'agir de
fichiers de logs, de données applications, de fichiers média envoyés par
vos utilisateurs, de vidéos et images ou de données de sauvegardes.
\end{description}
\textbf{\url{https://aws.amazon.com/fr/s3/}.}
\includegraphics{images/amazon-s3-arch.png}

View File

@ -1,7 +1,9 @@
\documentclass[twoside=no,parskip=half,numbers=enddot,bibliography=totoc,index=totoc,listof=totoc]{scrbook}
\usepackage{makeidx}
\usepackage[utf8]{inputenc}
\usepackage[french]{babel}
\usepackage{csquotes}
\usepackage[acronym]{glossaries}
\usepackage{hyperref}
\usepackage{setspace}
\usepackage{listing}
@ -11,8 +13,8 @@
\usepackage{float}
\usepackage[export]{adjustbox}
\renewcommand\listlistingname{Liste des morceaux de code}
\renewcommand\listlistingname{Liste des morceaux de code}
\onehalfspacing
@ -45,27 +47,17 @@
\include{parts/environment.tex}
\include{chapters/maintenability.tex}
\include{chapters/architecture.tex}
\include{chapters/tests.tex}
\include{chapters/tools.tex}
\include{chapters/working-in-isolation.tex}
\include{chapters/python.tex}
\include{chapters/new-project.tex}
\include{parts/principles.tex}
\part{Principes fondamentaux de Django}
\chapter{Modélisation}
\chapter{Migrations}
\chapter{Shell}
\include{chapters/models.tex}
\include{chapters/migrations.tex}
\chapter{Administration}
@ -134,6 +126,8 @@
\bibliography{references}
\bibliographystyle{plainnat}
\include{glossary.tex}
\include{chapters/thanks.tex}
\end{document}

29
parts/principles.tex Normal file
View File

@ -0,0 +1,29 @@
\part{Principes fondamenteux de Django}
Dans cette partie, nous allons parler de plusieurs concepts fondamentaux au développement rapide d'une application utilisant Django.
Nous parlerons de modélisation, de métamodèle, de migrations, d'administration auto-générée, de traductions et de cycle de vie des données.
Django est un framework Web qui propose une très bonne intégration des composants et une flexibilité bien pensée: chacun des composants permet de définir son contenu de manière poussée, en respectant des contraintes
logiques et faciles à retenir, et en gérant ses dépendances de manière autonome.
Pour un néophyte, la courbe d'apprentissage sera relativement ardue: à côté de concepts clés de Django, il conviendra également d'assimiler correctement les structures de données du langage Python, le cycle de vie des requêtes HTTP et le B.A-BA des principes de sécurité.
En restant dans les sentiers battus, votre projet suivra un patron de conception dérivé du modèle \texttt{MVC} (Modèle-Vue-Controleur), où la variante concerne les termes utilisés: Django les nomme respectivement
Modèle-Template-Vue et leur contexte d'utilisation.
Dans un \textbf{pattern} MVC classique, la traduction immédiate du \textbf{contrôleur} est une \textbf{vue}. Et comme nous le verrons par la suite, la \textbf{vue} est en fait le \textbf{template}.
La principale différence avec un modèle MVC concerne le fait que la vue ne s'occupe pas du routage des URLs; ce point est réalisé par un autre composant, interne au framework, graĉe aux différentes routes définies
dans les fichiers \texttt{urls.py}.
\begin{itemize}
\item
Le \textbf{modèle} (\texttt{models.py}) fait le lien avec la base de données et permet de définir les champs et leur type à associer à une table.
\emph{Grosso modo}*, une table SQL correspondra à une classe d'un modèle Django.
\item
La \textbf{vue} (\texttt{views.py}), qui joue le rôle de contrôleur: \emph{a priori}, tous les traitements, la récupération des données, etc. doit passer par ce composant et ne doit (pratiquement) pas être généré à la volée, directement à l'affichage d'une page.
En d'autres mots, la vue sert de pont entre les données gérées par la base et l'interface utilisateur.
\item
Le \textbf{template}, qui s'occupe de la mise en forme: c'est le composant qui s'occupe de transformer les données en un affichage compréhensible (avec l'aide du navigateur) pour l'utilisateur.
\end{itemize}
Pour reprendre une partie du schéma précédent, lorsqu'une requête est émise par un utilisateur, la première étape va consister à trouver une \emph{route} qui correspond à cette requête, c'est à dire à trouver la
correspondance entre l'URL qui est demandée par l'utilisateur et la fonction du langage qui sera exécutée pour fournir le résultat attendu.
Cette fonction correspond au \textbf{contrôleur} et s'occupera de construire le \textbf{modèle} correspondant.