... to \ldots
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-05-11 20:01:39 +02:00
parent bc7c7f74d4
commit ad625a1b40
16 changed files with 141 additions and 144 deletions

View File

@ -289,7 +289,4 @@ class PeopleSerializer(serializers.HyperlinkedModelSerializer):
\end{minted}
Nous ne faisons donc bien que redéfinir la propriété
\texttt{contract\_set} et indiquons qu'il s'agit à présent d'une
instance de \texttt{ContractSerializer}, et qu'il est possible d'en
avoir plusieurs. C'est tout.
Nous ne faisons donc bien que redéfinir la propriété \texttt{contract\_set} et indiquons qu'il s'agit à présent d'une instance de \texttt{ContractSerializer}, et qu'il est possible d'en avoir plusieurs. C'est tout.

View File

@ -98,7 +98,7 @@ Une méthode \texttt{render} permet également de proposer (très grossièrement
\end{listing}
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, ...), il sera nécessaire de modifier la classe \texttt{Document}.
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, \ldots) il sera nécessaire de modifier la classe \texttt{Document}.
Ceci n'est:
\begin{enumerate}
@ -143,7 +143,7 @@ En suivant le principe de responsabilité unique, une bonne pratique consiste à
A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, il nous suffira de modifier la classe \texttt{DocumentRenderer}, sans que la classe \texttt{Document} ne soit impactée.
En même temps, le jour où une instance de type \texttt{Document} sera liée à un champ \texttt{author}, rien ne dit que le rendu devra en tenir compte; nous modifierons donc notre classe pour y ajouter le nouveau champ sans que cela n'impacte nos différentes manières d'effectuer un rendu.
Un autre exemple consiterait à faire communiquer une méthode avec une base de données: ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement spécifique (emplacement sur un disque, ...): cette action doit être prise en compte par une autre classe (ou un autre concept ou composant), qui s'occupera de définir elle-même l'emplacement où l'évènement sera enregistré, que ce soit dans une base de données, une instance Graylog ou un fichier.
Un autre exemple consiterait à faire communiquer une méthode avec une base de données: ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement spécifique (emplacement sur un disque, \ldots): cette action doit être prise en compte par une autre classe (ou un autre concept ou composant), qui s'occupera de définir elle-même l'emplacement où l'évènement sera enregistré, que ce soit dans une base de données, une instance Graylog ou un fichier.
Cette manière de structurer le code permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce qui augmente ainsi la testabilité globale du projet.
L'équivalent du principe de responsabilité unique au niveau des composants sera le \texttt{Common Closure Principle} \index{CCP}.
@ -335,7 +335,7 @@ Petit exemple pratique: si nous définissons une méthode \texttt{make\_some\_no
def make_some_noise(self):
print("Roaaar!")
\end{minted}
\caption{Un lion et un canard sont sur un bateau...}
\caption{Un lion et un canard sont sur un bateau\ldots}
\end{listing}
Le principe de substitution de Liskov suggère qu'une classe doit toujours pouvoir être considérée comme une instance de sa classe parente, et \textbf{doit pouvoir s'y substituer}.
@ -365,7 +365,7 @@ Pour revenir à nos exemples de rendus de documents, nous aurions pu faire héri
import markdown
return markdown.markdown(document.content)
\end{minted}
\caption{Le convertisseur Markdown hérite d'un convertisseur XML...}
\caption{Le convertisseur Markdown hérite d'un convertisseur XML\ldots}
\end{listing}
Si nous décidons à un moment d'ajouter une méthode d'entête au niveau de notre classe de rendu XML, notre rendu en Markdown héritera irrémédiablement de cette même méthode:
@ -397,7 +397,7 @@ Si nous décidons à un moment d'ajouter une méthode d'entête au niveau de not
\caption{\ldots~ et il a mal à l'entête}
\end{listing}
Le code ci-dessus ne porte pas à conséquence \footnote{Pas immédiatement, en tout cas...}, mais dès que nous invoquerons la méthode \texttt{header()} sur une instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de déclaration XML (\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un fichier Markdown, ce qui n'aura aucun sens.
Le code ci-dessus ne porte pas à conséquence \footnote{Pas immédiatement, en tout cas\ldots}, mais dès que nous invoquerons la méthode \texttt{header()} sur une instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de déclaration XML (\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un fichier Markdown, ce qui n'aura aucun sens.
En revenant à notre proposition d'implémentation, suite au respect d'Open-Closed, une solution serait de n'implémenter la méthode \texttt{header()} qu'au niveau de la classe \texttt{XmlRenderer}:
@ -551,7 +551,7 @@ Lobjectif est donc de découpler ces différentes fonctionnalités en plusieu
\end{listing}
Cette réflexion s'applique à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, les variables déclarées, ...
Cette réflexion s'applique à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, les variables déclarées, \ldots
Quel que soit le composant que l'on utilise ou analyse, il est plus qu'intéressant de se limiter uniquement à ce dont nous avons besoin plutôt que d'embarquer le must absolu qui peut faire 1000x fonctions de plus que n'importe quel autre produit, alors que seules deux d'entre elles seront nécessaires.
En Python, ce comportement est inféré lors de l'exécution, et donc pas vraiment d'application pour ce contexte d'étude: de manière plus générale, les langages dynamiques sont plus flexibles et moins couplés que les langages statiquement typés, pour lesquels l'application de ce principe-ci permettrait de juste mettre à jour une DLL ou un JAR sans que cela n'ait d'impact sur le reste de l'application.

View File

@ -2,4 +2,4 @@
\section{Waffle}
\section{...}
\section{\ldots}

View File

@ -94,7 +94,7 @@ Cela permet de valider des données reçues en entrée et d'afficher (très) fac
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, ...
modifier les types de contrôleurs, les placer au pixel près, \ldots
Tout ça demande énormément de temps.
Et c'est là qu'intervient \href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}.
Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le \textbf{layout} et la présentation.
@ -118,5 +118,5 @@ Pour chaque champ, crispy-forms va :
\section{Conclusions}
Toute donnée entrée par l'utilisateur, quelle qu'elle soit, \textbf{doit} passer par une instance de \texttt{form}: qu'il s'agisse d'un formulaire HTML, d'un fichier CSV, d'un parser XML, ...
Toute donnée entrée par l'utilisateur, quelle qu'elle soit, \textbf{doit} passer par une instance de \texttt{form}: qu'il s'agisse d'un formulaire HTML, d'un fichier CSV, d'un parser XML, \ldots
Dès que des informations "de l'extérieur" font mine d'arriver dans le périmètre de votre application, il convient d'appliquer immédiatement des principes de sécurité reconnus.

View File

@ -52,7 +52,7 @@ Il s'agit des composants comme le proxy inverse ou les distributeurs de charge:
\begin{quote}
Focusing on resilience often means that a firm can handle events that may cause crises for most organizations in a manner that is routine and mundane.
Specific architectural patterns that they implemented included fail fasts (settings aggressive timeouts such that failing components don't make the entire system crawl to a halt), fallbacks (designing each feature to degrade or fall back to a lower quality representation), and feature removal (removing non-critical features when they run slowly from any given page to prevent them from impacting the member experience).
Another astonishing example of the resilience that the netflix team created beyond preserving business continuity during the AWS outage, was that they went over six hours into the AWS outage before declaring a Sev 1 incident, assuming that AWS service would eventually be restored (i.e., "AWS will come back... it usually does, right ?").
Another astonishing example of the resilience that the netflix team created beyond preserving business continuity during the AWS outage, was that they went over six hours into the AWS outage before declaring a Sev 1 incident, assuming that AWS service would eventually be restored (i.e., "AWS will come back\ldots it usually does, right ?").
Only after six hours into the outage did they activate any business continuity procedures. \cite[p. 282]{devops_handbook}
\end{quote}
@ -153,7 +153,7 @@ L'analyse de ces métriques garantit un juste retour d'informations, sous réser
La première étape consiste à agréger ces données dans un dépôt centralisé, tandis que la seconde étape exigera une structuration correcte des données envoyées.
La collecte des données doit récupérer des données des couches métiers, applicatives et d'environnement.
Ces données couvrent les évènements, les journaux et les métriques - indépendamment de leur source - le pourcentage d'utilisation du processeur, la mémoire utilisée, les disques systèmes, l'utilisation du réseau, ...
Ces données couvrent les évènements, les journaux et les métriques - indépendamment de leur source - le pourcentage d'utilisation du processeur, la mémoire utilisée, les disques systèmes, l'utilisation du réseau, \ldots
\begin{enumerate}
\item
@ -204,7 +204,7 @@ Chaque évènement est associé à un niveau; celui-ci indique sa criticité et
\textbf{DEBUG}: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application.
Généralement, ce niveau est désactivé pour une application qui passe en production, sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement.
\item
\textbf{INFO}: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, ...
\textbf{INFO}: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, \ldots
\item
\textbf{WARN}: Regroupe les informations qui pourraient potentiellement devenir des erreurs.
\item
@ -262,7 +262,7 @@ exemples}.
Il existe également \href{https://munin-monitoring.org}{Munin}, \href{https://www.elastic.co}{Logstash, ElasticSearch et Kibana (ELK-Stack)} ou \href{https://www.fluentd.org}{Fluentd}.
\subsubsection{Zabbix, Nagios, ...}
\subsubsection{Zabbix, Nagios, \ldots}
\subsubsection{Sentry}

View File

@ -1,3 +1,3 @@
\chapter{Kubernetes}
Voir ici https://www.youtube.com/watch?v=NAOsLaB6Lfc ( La vidéo dure 5h... )
Voir ici https://www.youtube.com/watch?v=NAOsLaB6Lfc ( La vidéo dure 5h\ldots )

View File

@ -73,7 +73,7 @@ Ce dépôt n'est pas uniquement destiné à hébergé le code source, mais égal
Chaque installation ou configuration doit toujours être faite de la même manière, et doit pouvoir être répétée quel que soit l'environnement cible.
Ceci permet d'éviter que l'application n'utilise une dépendance qui ne soit déjà installée sur un des sytèmes de développement, et qu'elle soit difficile, voire impossible, à répercuter sur un autre environnement.
Dans le cas de Python, cela pourra être fait au travers de \href{https://pypi.org/project/pip/}{PIP - Package Installer for Python} ou \href{https://python-poetry.org/}{Poetry}.
La majorité des langages moderners proposent des mécanismes similaires (\href{https://rubygems.org/}{Gem} pour Ruby, \href{https://www.npmjs.com/}{NPM} pour NodeJS, ...)
La majorité des langages moderners proposent des mécanismes similaires (\href{https://rubygems.org/}{Gem} pour Ruby, \href{https://www.npmjs.com/}{NPM} pour NodeJS, \ldots)
Dans tous les cas, chaque application doit disposer d'un environnement sain, qui lui est assigné. Vu le peu de ressources que cela coûte, il ne faut pas s'en priver.
Chaque dépendance devra déclarer et épingler dans un fichier la version nécessaire.
@ -95,11 +95,11 @@ que:
\item \ldots
\end{enumerate}
En pratique, toute information susceptible d'évoluer ou de changer (un seuil, une ressource externe, un couple utilisateur/mot de passe, ...) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable.
En pratique, toute information susceptible d'évoluer ou de changer (un seuil, une ressource externe, un couple utilisateur/mot de passe, \ldots) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable.
Ceci permet de paramétrer facilement un environnement (par exemple, un container), simplement en modifiant une variable de configuration qui spécifierait la base de données sur laquelle l'application devra se connecter.
Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, nous ne devons trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code. \footnote{Ainsi, nous pourrions faire une \href{https://github.com/search?q=filenamefilename:.env DB_USERNAME&type=Code}{recherche sur Github} pour retrouver certaines variables d'environnement qui auraient été laissées en dur dans le code source de certains projets. Le \href{https://github.com/techgaun/github-dorks}{dépôt suivant} liste quelques idées de variables à rechercher...}.
Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, \ldots) sera définie directement au niveau de l'hôte - à aucun moment, nous ne devons trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code. \footnote{Ainsi, nous pourrions faire une \href{https://github.com/search?q=filenamefilename:.env DB_USERNAME&type=Code}{recherche sur Github} pour retrouver certaines variables d'environnement qui auraient été laissées en dur dans le code source de certains projets. Le \href{https://github.com/techgaun/github-dorks}{dépôt suivant} liste quelques idées de variables à rechercher\ldots}.
Au moment de développer une nouvelle fonctionnalité, réfléchissez si
l'un des paramètres utilisés risquerait de subir une modification ou s'il concerne un principe de sécurité: ce composant peut concerner une nouvelle chaîne de connexion, un point de terminaison nécessaire à télécharger des données officielles ou un chemin vers un répertoire partagé pour y déposer un fichier.
@ -109,13 +109,13 @@ Par exemple, Gitea expose \href{https://docs.gitea.io/en-us/config-cheat-sheet/}
\section{Ressources externes}
Nous parlons de bases de données, de services de mise en cache, d'API externes, ... L'application doit être capable d'effectuer des changements au niveau de ces ressources sans que son code ne soit modifié.
Nous parlons de bases de données, de services de mise en cache, d'API externes, \ldots L'application doit être capable d'effectuer des changements au niveau de ces ressources sans que son code ne soit modifié.
Nous parlons alors de \textbf{ressources attachées}, dont la présence est nécessaire au bon fonctionnement de l'application, mais pour lesquelles le \textbf{type} n'est pas obligatoirement défini.
Nous voulons par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached".
De cette manière, les ressources peuvent être attachées et détachées d'un déploiement à la volée.
Si une base de données ne fonctionne pas correctement (problème matériel ?), l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ... directement \emph{via} des variables d'environnement.
Si une base de données ne fonctionne pas correctement (problème matériel ?), l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, \ldots directement \emph{via} des variables d'environnement.
\includegraphics{images/12factors/attached-resources.png}
@ -140,7 +140,7 @@ Ces ressources sont donc spécifiés grâce à des variables d'environnement, et
\includegraphics{images/12factors/release.png}
Parmi les solutions possibles, nous pourrions nous pourrions nous baser
sur les \emph{releases} de Gitea, sur un serveur d'artefacts (\href{https://fr.wikipedia.org/wiki/Capistrano_(logiciel)}{Capistrano}), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, ...).
sur les \emph{releases} de Gitea, sur un serveur d'artefacts (\href{https://fr.wikipedia.org/wiki/Capistrano_(logiciel)}{Capistrano}), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, \ldots).
\section{Mémoire des processus d'exécution}
@ -168,7 +168,7 @@ Le serveur (= l'hôte) choisit d'appliquer une correspondance entre "son" port 4
\section{Connaissance et confiance des processus systèmes}
Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus \emph{stateless} (sans état). Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des particuliers pour certaines tâches: requêtes HTTP \emph{via} des processus Web; \emph{long-running} jobs pour des processus asynchrones, ...
Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus \emph{stateless} (sans état). Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des particuliers pour certaines tâches: requêtes HTTP \emph{via} des processus Web; \emph{long-running} jobs pour des processus asynchrones, \ldots
Si cela existe sur l'hôte hébergeant l'application, ne vous fatiguez pas: utilisez le.
\includegraphics{images/12factors/process-types.png}

View File

@ -1,15 +1,15 @@
\chapter{Migrations}
Dans cette section, nous allons voir comment fonctionnent les 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.
L'intégration des migrations a été réalisée dans la version 1.7 de Django.
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.
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.
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:
@ -43,13 +43,13 @@ En bref, les problèmes suivants apparaissent très rapidement:
\textbf{Manque d'automatisation possible}, à moins d'écrire un programme, qu'il
faudra également maintenir et intégrer au niveau des tests
\item
\textbf{Nécessité de maintenir des scripts} différents, en fonction des
\textbf{Nécessité de maintenir des scripts} différents, en fonction des
moteurs de base de données supportés
\item
\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 qui doivent être appliquées.
@ -78,7 +78,7 @@ class Book(models.Model):
category = models.ManyManyField(Category, on_delete=models.CASCADE)
\end{minted}
Chronologiquement, cela nous a donné
Chronologiquement, cela nous a donné
\begin{enumerate}
\item Une première migration consistant à créer le modèle initial
@ -148,15 +148,15 @@ Chronologiquement, cela nous a donné
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)
@ -164,12 +164,12 @@ class Category(models.Model):
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category)
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.
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}
@ -195,15 +195,15 @@ class Migration(migrations.Migration):
\begin{itemize}
\item
La migration supprime l'ancienne clé étrangère ...
La migration supprime l'ancienne clé étrangère \ldots
\item
... et ajoute une nouvelle table, permettant de lier nos catégories à nos livres.
\ldots 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}
@ -245,7 +245,7 @@ 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:
@ -266,10 +266,10 @@ Dès que nous les appliquerons, nous recevrons les messages suivants:
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
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.
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}
@ -442,7 +442,7 @@ class Migration(migrations.Migration):
field=models.TextField(blank=True),
),
]
\end{minted}
\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}:
@ -453,15 +453,15 @@ La commande \texttt{python\ manage.py\ squashmigrations\ library\ 0002\ 0003} ap
- 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,
@ -517,5 +517,5 @@ 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}
\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\ldots Ce qui n'est pas gagné.
\end{enumerate}

View File

@ -2,20 +2,20 @@
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.
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.
Comme expliqué par Aurélie Jean \cite{other_side}, "\emph{toute modélisation reste une approximation de la réalité}".
Comme expliqué par Aurélie Jean \cite{other_side}, "\emph{toute modélisation reste une approximation de la réalité}".
\begin{quote}
\emph{Le modèle n'est qu'une grande hypothèse.
\emph{Le modèle n'est qu'une grande hypothèse.
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.} \cite{other_side}
\end{quote}
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 objet.
table SQL, tout en respectant une approche propre à la programmation orientée objet.
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 projets; 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.
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 projets; 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. \footnote{Et dans ce cas, il y a des alternatives comme Flask qui permettent une flexibilité et un choix des composants beaucoup plus grand.}
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 à:
@ -33,7 +33,7 @@ Conceptuellement, c'est pourtant la manière de faire qui permettra d'avoir quel
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: 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.
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: 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.
@ -55,7 +55,7 @@ L'exemple ci-dessous présente trois structure de données, qui exposent chacune
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
@ -82,7 +82,7 @@ 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
@ -91,7 +91,7 @@ class Geometry:
return shape.height * shape.width
if isinstance(shape, Circle):
return PI * shape.radius**2
return PI * shape.radius**2
raise NoSuchShapeException()
\end{minted}
@ -124,7 +124,7 @@ 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
@ -132,32 +132,32 @@ class Circle(Shape):
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.
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}.
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}.
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()}, ...
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()}, \ldots
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é.
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 les moteurs de base de données relationnelles.
Dans ce domaine, un point d'attention est de toujours disposer d'une clé primaire pour nos enregistrements.
Dans ce domaine, 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}.
convention) \texttt{id}.
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}.
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}.
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 à un primitif, soit à une autre instance au travers de sa clé d'identification.
@ -219,7 +219,7 @@ Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs cat
\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}.
\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.
@ -227,7 +227,7 @@ Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs cat
Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie supplémentaire "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 d'un livre puisse être liée à plusieurs catégories.
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour qu'une occurrence d'un livre puisse être liée à plusieurs catégories.
Au lieu d'utiliser un champ de type \texttt{ForeignKey}, nous utiliserons à présent un champ de type \texttt{ManyToMany}, c'est-à-dire qu'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}
@ -251,7 +251,7 @@ Le \texttt{shell} est un environnement REPL \index{REPL} identique à ce que l'i
\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.
Il se démarre grâce à la commande \texttt{python manage.py shell}, et donne un accès intuitif \footnote{Pour un développeur\ldots} à l'ensemble des informations disponibles.
\subsection{Accès aux relations}
@ -278,7 +278,7 @@ 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.
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:
@ -319,14 +319,14 @@ class Runner(models.Model):
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,
default=Zone.ZONE_5,
help_text="What was your best time on the marathon in last 2 years?"
)
)
\end{minted}
\section{Validateurs}
La validation des champs intervient sur toute donnée entrée via le modèle.
La validation des champs intervient sur toute donnée entrée via le modèle.
Cette validation dispose de trois niveaux:
\begin{enumerate}
@ -337,7 +337,7 @@ Cette validation dispose de trois niveaux:
\subsection{Validation d'un champ}
La première manière de valider le contenu d'un champ est aussi la plus simple.
La première manière de valider le contenu d'un champ est aussi la plus simple.
En prenant un modèle type:
\begin{minted}{python}
@ -361,7 +361,7 @@ Ca, c'est le deuxième niveau. Le contexte donne accès à déjà plus d'informa
raise ValidationError('Title does not start with T')
\end{minted}
Il n'est plus nécessaire de définir d'attribut \texttt{validators=[...]}, puisque Django va appliquer un peu d'introspection pour récupérer toutes les méthodes qui commencent par \texttt{clean\_} et pour les faire correspondre au nom du champ à valider (ici, \texttt{title}).
Il n'est plus nécessaire de définir d'attribut \texttt{validators=[\ldots]}, puisque Django va appliquer un peu d'introspection pour récupérer toutes les méthodes qui commencent par \texttt{clean\_} et pour les faire correspondre au nom du champ à valider (ici, \texttt{title}).
\subsection{Clean}
@ -376,7 +376,7 @@ Ici, c'est global: nous pouvons valider des données ou des champs globalement v
\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: nous pouvons passer par un \texttt{ModelManager} pour limiter le couplage; l'accès à une information stockée en base de données ne se fait dès lors qu'au travers de cette instance et pas directement au travers du modèle.
Mieux encore: nous pouvons passer par un \texttt{ModelManager} pour limiter le couplage; l'accès à une information stockée en base de données ne se fait dès lors qu'au travers de cette instance et pas directement au travers du modèle.
\begin{minted}{python}
class ItemManager(...):
@ -385,14 +385,14 @@ class ItemManager(...):
\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\ (...)}.
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\ (\ldots)}.
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...
Nous pourrions penser qu'utiliser un itérateur permettrait de combiner les deux, mais ce n'est pas le cas\ldots
Comme l'indique la documentation:
@ -448,7 +448,7 @@ Soit encore combiner des filtres:
\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}
@ -484,7 +484,7 @@ queryset, soit l'opérateur \texttt{\textasciitilde{}} sur un Q object;
\item
\url{http://blog.etianen.com/blog/2013/06/08/django-querysets/}
\end{itemize}
Deux solutions:
\begin{enumerate}
@ -492,7 +492,7 @@ Deux solutions:
Prefetch
\item
select\_related
\end{enumerate}
\end{enumerate}
\subsection{Indices}
@ -504,7 +504,7 @@ Après analyse seulement.
\section{Métamodèle et introspection}
Comme chaque classe héritant de \texttt{models.Model} possède une propriété \texttt{objects}.
Comme chaque classe héritant de \texttt{models.Model} possède une propriété \texttt{objects}.
Cette propriété permet d'accéder aux objects persistants dans la base de données, au travers d'un \texttt{ModelManager}.
Les propriétés de la classe Meta les plus utiles sont les suivates:
@ -526,16 +526,16 @@ Les propriétés de la classe Meta les plus utiles sont les suivates:
\begin{minted}{python}
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',)
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}).
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()}
Pour sélectionner un objet au pif: \texttt{return\ Category.objects.order\_by("?").first()}
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.
Cela signifie que le \texttt{top 1} utilisé dans le code pourrait être impacté en cas de modification de cette propriété \texttt{ordering}, ce qui corrobore la nécessité de \emph{tester le code dans son ensemble}.
@ -543,7 +543,7 @@ Cela signifie que le \texttt{top 1} utilisé dans le code pourrait être impact
\subsection{Représentation textuelle}
verbose\_name et verbose\_name\_plural + lien avec
verbose\_name et verbose\_name\_plural + lien avec
\subsection{Contraintes}
@ -564,7 +564,7 @@ constraints = [ # constraints added
\section{Querysets et managers}
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
@ -577,8 +577,8 @@ L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propo
(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.
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.
@ -589,7 +589,7 @@ Si on veut connaître la requête SQL sous-jacente à l'exécution du queryset,
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:
@ -625,9 +625,9 @@ Par défaut, le gestionnaire est accessible au travers de la propriété
\section{Refactoring et héritages}
On constate que plusieurs classes possèdent les mêmes propriétés \texttt{created\_at} et \texttt{updated\_at}, initialisées aux mêmes valeurs.
On constate que plusieurs classes possèdent les mêmes propriétés \texttt{created\_at} et \texttt{updated\_at}, initialisées aux mêmes valeurs.
Pour gagner en cohérence, nous allons créer une classe dans laquelle nous définirons ces deux champs, et nous ferons en sorte que les classes \texttt{Wishlist}, \texttt{Item} et \texttt{Part} en
héritent.
héritent.
Django gère trois sortes d'héritage:
\begin{itemize}
@ -654,19 +654,19 @@ chacune des classes filles.
class AbstractModel(models.Model):
class Meta:
abstract = True
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Wishlist(AbstractModel):
pass
class Item(AbstractModel):
pass
class Part(AbstractModel):
pass
\end{minted}
@ -793,21 +793,21 @@ Nous pourrions ainsi définir les classes suivantes:
wishlist.expiration_date = expiration_date
wishlist.save()
return wishlist
class ChristmasWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
christmas = datetime(current_year, 12, 31)
w = Wishlist.create(name, description, christmas)
w.save()
class EasterWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
expiration_date = datetime(current_year, 4, 1)
@ -817,13 +817,13 @@ Nous pourrions ainsi définir les classes suivantes:
\section{Conclusions}
Le modèle proposé par Django est un composant extrêmement performant, mais fort couplé avec le coeur du framework.
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 exemples 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.
Dans les exemples 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

@ -117,10 +117,10 @@ Nous faisons ici une distinction entre un \textbf{projet} et une \textbf{applica
\begin{itemize}
\item
\textbf{Un projet} représente l'ensemble des applications, paramètres, middlewares, dépendances, ..., qui font que votre code fait ce qu'il est sensé faire.
\textbf{Un projet} représente l'ensemble des applications, paramètres, middlewares, dépendances, \ldots, qui font que votre code fait ce qu'il est sensé faire.
Il s'agit grosso modo d'un câblage de tous les composants entre eux.
\item
\textbf{Une application} est un contexte d'exécution (vues, comportements, pages HTML, ...), idéalement autonome, d'une partie du projet.
\textbf{Une application} est un contexte d'exécution (vues, comportements, pages HTML, \ldots), idéalement autonome, d'une partie du projet.
Une application est supposée avoir une portée de réutilisation, même s'il ne sera pas toujours possible de viser une généricité parfaite.
\end{itemize}
@ -155,7 +155,7 @@ En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la gestion de
Ceci pourrait être commun aux deux projets.
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}, ...
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}, \ldots
Le projet s'occupe principalement d'appliquer une couche de glue entre différentes applications.
@ -186,7 +186,7 @@ catégories:
\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{conformité} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, ...
\textbf{django}: vérifier la \textbf{conformité} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, \ldots
Ce sont des commandes d'administration générale.
\item
\textbf{sessions}: suppressions des sessions en cours
@ -264,12 +264,12 @@ 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} ! \footnote{Il manque quelques fichiers utiles, qui seront décrits par la suite, pour qu'une application soit réellement autonome: templates, \texttt{urls.py}, managers, services, ...}
Notre application a bien été créée, et nous l'avons déplacée dans le répertoire \texttt{gwift} ! \footnote{Il manque quelques fichiers utiles, qui seront décrits par la suite, pour qu'une application soit réellement autonome: templates, \texttt{urls.py}, managers, services, \ldots}
\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 actions à réaliser : requêtes en bases de données, construction de la page, ...
Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actions à réaliser : requêtes en bases de données, construction de la page, \ldots
La recherche d'une solution à 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 frameworks) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ repeat\ yourself} \footnote{DRY}.
@ -402,7 +402,7 @@ TODO : passer à poetry
\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, ...
base de données, \ldots
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
@ -468,7 +468,7 @@ On a deux choix ici:
\subsection{Couverture de code}
Quel que soit le framework de tests choisi (django-tests, pytest, unittest, ...), la couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests.
Quel que soit le framework de tests choisi (django-tests, pytest, unittest, \ldots), la couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests.
Il ne s'agit pas de vérifier que le code est bien testé, mais de vérifier quelle partie du code est testée.
Le paquet coverage se charge dévaluer le pourcentage de code couvert par les tests.
Avec pytest, il convient dutiliser le paquet pytest-cov, suivi de la commande pytest

View File

@ -783,4 +783,4 @@ facteurs → Construction du fichier setup.cfg
django_coverage_plugin
\end{verbatim}
Mypy + black + pylint + flake8 + pyflakes + ...
Mypy + black + pylint + flake8 + pyflakes + \ldots

View File

@ -22,7 +22,7 @@ Notre première vue permettra de récupérer la liste des objets de type \texttt
Supposez que cette liste soit accessible \textit{via} la clé \texttt{wishlists} d'un dictionnaire passé au template.
Cette liste devient dès lors accessible grâce aux tags \texttt{\{\% for wishlist in wishlists \%\}}.
A chaque tour de boucle, nous pourrons directement accéder à la variable \texttt{\{\{ wishlist \}\}}.
De même, il sera possible d'accéder aux propriétés de cette objet de la même manière: \texttt{\{\{ wishlist.id \}\}}, \texttt{\{\{ wishlist.description \}\}}, ... et d'ainsi respecter la mise en page que nous souhaitons.
De même, il sera possible d'accéder aux propriétés de cette objet de la même manière: \texttt{\{\{ wishlist.id \}\}}, \texttt{\{\{ wishlist.description \}\}}, \ldots et d'ainsi respecter la mise en page que nous souhaitons.
En reprenant l'exemple de la page HTML définie ci-dessus, nous pouvons l'agrémenter de la manière suivante:

View File

@ -175,7 +175,7 @@ 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...
Martin Fowler observes that, in general, "a ten minute build [and test process] is perfectly within reason\ldots
[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.
@ -204,7 +204,7 @@ Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants.
Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante.
Il existe plusieurs types de tests (intégration, comportement, ...)
Il existe plusieurs types de tests (intégration, comportement, \ldots)
Avoir des tests, c'est bien.
S'assurer que tout est testé, c'est mieux.
@ -220,7 +220,7 @@ Le paquet \texttt{coverage} se charge d'évaluer le pourcentage de code couvert
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, ...).
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, \ldots).
\subsection{Tests d'intégration}

View File

@ -263,7 +263,7 @@ Ce qui est quand même 'achement plus simple que d'appréhender tout un
\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 développement aussi proche que celui en production.
Par "\emph{système de virtualisation}", nous entendons n'importe quel application, système d'exploitation, système de containeurisation, \ldots qui permette de créer ou recréer un environnement de développement aussi proche que celui en production.
Les solutions sont nombreuses:
\begin{itemize}

View File

@ -41,7 +41,7 @@ Pour que nos pages soient un peu plus *eye-candy* que ce qu'on a présenté ci-d
<img src="{% static 'img/gwift-20x20.png' %}" />
</a>
</div>
<div class="collapse navbar-collapse" id="menuNavbar">
<div class="collapse navbar-collapse" id="menuNavbar">
{% include "_menu_items.html" %}
</div>
</div>
@ -59,7 +59,7 @@ Pour que nos pages soient un peu plus *eye-candy* que ce qu'on a présenté ci-d
</div>
</div>
<!-- end content -->
<!-- footer -->
<footer class="footer">
{% include "_footer.html" %}
@ -71,11 +71,11 @@ Pour que nos pages soient un peu plus *eye-candy* que ce qu'on a présenté ci-d
Quelques remarques:
* La première ligne du fichier inclut le *tag* `{% load staticfiles %}`. On y reviendra par la suite, mais en gros, cela permet de faciliter la gestion des fichiers statiques, notamment en les appelent grâce à la commande `{% static 'img/header.png' %}` ou `{% static 'css/app_style.css' %}`.
* La première ligne du fichier inclut le *tag* `{% load staticfiles %}`. On y reviendra par la suite, mais en gros, cela permet de faciliter la gestion des fichiers statiques, notamment en les appelent grâce à la commande `{% static 'img/header.png' %}` ou `{% static 'css/app_style.css' %}`.
* La balise `<head />` est bourée d'appel vers des ressources stockées sur des :abbr:`CDN (Content Delivery Networks)`.
* Les balises `{% block content %} {% endblock %}` permettent de faire hériter du contenu depuis une autre page. On l'utilise notamment dans notre page `templates/wish/list.html`.
* Pour l'entête et le bas de page, on fait appel aux balises `{% include 'nom_du_fichier.html' %}`: ces fichiers sont des fichiers physiques, placés sur le filesystem, juste à côté du fichier `base.html`. De façon bête et méchante, cela inclut juste du contenu HTML. Le contenu des fichiers `_menu_items.html` et `_footer.html` est copié ci-dessous.
[source,html]
----
<!-- gwift/templates/wish/list.html -->
@ -92,7 +92,7 @@ Quelques remarques:
{% endblock %}
----
[source,html]
----
<!-- gwift/templates/_menu_items.html -->
@ -120,9 +120,9 @@ Quelques remarques:
</div>
----
En fonction de vos affinités, vous pourriez également passer par `PluCSS <http://plucss.pluxml.org/>`_, `Pure <http://purecss.io/>`_, `Knacss <http://knacss.com/>`_, `Cascade <http://www.cascade-framework.com/>`_, `Semantic <http://semantic-ui.com/>`_ ou `Skeleton <http://getskeleton.com/>`_. Pour notre plus grand bonheur, les frameworks de ce type pullulent. Reste à choisir le bon.
En fonction de vos affinités, vous pourriez également passer par `PluCSS <http://plucss.pluxml.org/>`_, `Pure <http://purecss.io/>`_, `Knacss <http://knacss.com/>`_, `Cascade <http://www.cascade-framework.com/>`_, `Semantic <http://semantic-ui.com/>`_ ou `Skeleton <http://getskeleton.com/>`_. Pour notre plus grand bonheur, les frameworks de ce type pullulent. Reste à choisir le bon.
*A priori*, si vous relancez le serveur de développement maintenant, vous devriez déjà voir les modifications... Mais pas les images, ni tout autre fichier statique.
*A priori*, si vous relancez le serveur de développement maintenant, vous devriez déjà voir les modifications\ldots Mais pas les images, ni tout autre fichier statique.
==== Fichiers statiques
@ -132,11 +132,11 @@ Si vous ouvrez la page et que vous lancez la console de développement (F12, sur
* `/static/img/favicon.ico`
* `/static/img/gwift-20x20.png`.
En fait, par défaut, les fichiers statiques sont récupérés grâce à deux handlers:
En fait, par défaut, les fichiers statiques sont récupérés grâce à deux handlers:
. `django.contrib.staticfiles.finders.FileSystemFinder` et . `django.contrib.staticfiles.finders.AppDirectoriesFinder`.
. `django.contrib.staticfiles.finders.FileSystemFinder` et . `django.contrib.staticfiles.finders.AppDirectoriesFinder`.
En fait, Django va considérer un répertoire `static` à l'intérieur de chaque application. Si deux fichiers portent le même nom, le premier trouvé sera pris. Par facilité, et pour notre développement, nous placerons les fichiers statiques dans le répertoire `gwift/static`. On y trouve donc:
En fait, Django va considérer un répertoire `static` à l'intérieur de chaque application. Si deux fichiers portent le même nom, le premier trouvé sera pris. Par facilité, et pour notre développement, nous placerons les fichiers statiques dans le répertoire `gwift/static`. On y trouve donc:
[source,bash]
----

View File

@ -44,7 +44,7 @@ Le serveur que Django met à notre disposition \emph{via} la commande \texttt{ru
En production:
\begin{itemize}
\item Il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...
\item Il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, \ldots
De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l'on connait ses limites et que l'on se limite à un utilisateur à la fois.
\item Il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanés.
En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données. En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou ou simplement parce que le serveur répondra trop lentement.