gwift-book/source/main.tex

10720 lines
456 KiB
TeX
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

% Options for packages loaded elsewhere
\PassOptionsToPackage{unicode}{hyperref}
\PassOptionsToPackage{hyphens}{url}
%
\documentclass[
]{article}
\usepackage{lmodern}
\usepackage{amssymb,amsmath}
\usepackage{ifxetex,ifluatex}
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{textcomp} % provide euro and other symbols
\else % if luatex or xetex
\usepackage{unicode-math}
\defaultfontfeatures{Scale=MatchLowercase}
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
\fi
% Use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
\IfFileExists{microtype.sty}{% use microtype if available
\usepackage[]{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
\makeatletter
\@ifundefined{KOMAClassName}{% if non-KOMA class
\IfFileExists{parskip.sty}{%
\usepackage{parskip}
}{% else
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
}{% if KOMA class
\KOMAoptions{parskip=half}}
\makeatother
\usepackage{xcolor}
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}}
\hypersetup{
pdftitle={Minor swing with Django},
pdfauthor={Cédric Declerfayt jaguarondi27@gmail.com; Fred Pauchet fred@grimbox.be},
hidelinks,
pdfcreator={LaTeX via pandoc}}
\urlstyle{same} % disable monospaced font for URLs
\usepackage{color}
\usepackage{fancyvrb}
\newcommand{\VerbBar}{|}
\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
% Add ',fontsize=\small' for more characters per line
\newenvironment{Shaded}{}{}
\newcommand{\AlertTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
\newcommand{\AnnotationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\AttributeTok}[1]{\textcolor[rgb]{0.49,0.56,0.16}{#1}}
\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\BuiltInTok}[1]{#1}
\newcommand{\CharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\CommentTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textit{#1}}}
\newcommand{\CommentVarTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\ConstantTok}[1]{\textcolor[rgb]{0.53,0.00,0.00}{#1}}
\newcommand{\ControlFlowTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
\newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.56,0.13,0.00}{#1}}
\newcommand{\DecValTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\DocumentationTok}[1]{\textcolor[rgb]{0.73,0.13,0.13}{\textit{#1}}}
\newcommand{\ErrorTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
\newcommand{\ExtensionTok}[1]{#1}
\newcommand{\FloatTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\FunctionTok}[1]{\textcolor[rgb]{0.02,0.16,0.49}{#1}}
\newcommand{\ImportTok}[1]{#1}
\newcommand{\InformationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
\newcommand{\NormalTok}[1]{#1}
\newcommand{\OperatorTok}[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
\newcommand{\OtherTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{#1}}
\newcommand{\PreprocessorTok}[1]{\textcolor[rgb]{0.74,0.48,0.00}{#1}}
\newcommand{\RegionMarkerTok}[1]{#1}
\newcommand{\SpecialCharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\SpecialStringTok}[1]{\textcolor[rgb]{0.73,0.40,0.53}{#1}}
\newcommand{\StringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\VariableTok}[1]{\textcolor[rgb]{0.10,0.09,0.49}{#1}}
\newcommand{\VerbatimStringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\WarningTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\usepackage{graphicx}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\makeatother
% Scale images if necessary, so that they will not overflow the page
% margins by default, and it is still possible to overwrite the defaults
% using explicit options in \includegraphics[width, height, ...]{}
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
% Set default figure placement to htbp
\makeatletter
\def\fps@figure{htbp}
\makeatother
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
\title{Minor swing with Django}
\author{Cédric Declerfayt
\href{mailto:jaguarondi27@gmail.com}{\nolinkurl{jaguarondi27@gmail.com}} \and Fred
Pauchet \href{mailto:fred@grimbox.be}{\nolinkurl{fred@grimbox.be}}}
\date{2022-02-20}
\begin{document}
\maketitle
\hypertarget{_licence}{%
\section{Licence}\label{_licence}}
Ce travail est licencié sous Attribution-NonCommercial 4.0 International
Attribution-NonCommercial 4.0 International
This license requires that reusers give credit to the creator. It allows
reusers to distribute, remix, adapt, and build upon the material in any
medium or format, for noncommercial purposes only.
\begin{itemize}
\item
\textbf{BY}: Credit must be given to you, the creator.
\item
\textbf{NC}: Only noncommercial use of your work is permitted.
Noncommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation.
\end{itemize}
\url{https://creativecommons.org/licenses/by-nc/4.0/?ref=chooser-v1}
La seule exception concerne les morceaux de code (non attribués),
disponibles sous licence \href{https://mit-license.org/}{MIT}.
\hypertarget{_pruxe9face}{%
\section{Préface}\label{_pruxe9face}}
\begin{quote}
The only way to go fast, is to go well
--- Robert C. Martin
\end{quote}
Nous n'allons pas vous mentir: il existe enormément de tutoriaux très
bien réalisés sur "\emph{Comment réaliser une application Django}" et
autres "\emph{Déployer votre code en 2 minutes}". Nous nous disions
juste que ces tutoriaux restaient relativement haut-niveaux et se
limitaient à un contexte donné, sans réellement préparer à la
maintenance et au suivi de l'application nouvellement développée.
L'idée du texte ci-dessous est de jeter les bases d'un bon
développement, en survolant l'ensemble des outils permettant de suivre
des lignes directrices reconnues, de maintenir une bonne qualité de code
au travers des différentes étapes menant jusqu'au déploiement et de
s'assurer du maintient correct de la base de code, en permettant à
n'importe qui de reprendre ce qui aura déjà été écrit.
Ces idées ne s'appliquent pas uniquement à Django et à son cadre de
travail, ni même au langage Python. Ces deux sujets sont cependant de
bons candidats et leur cadre de travail est bien défini, documenté et
suffisamment flexible.
Django se présente comme un \emph{Framework Web pour perfectionnistes
ayant des deadlines} cite:{[}django{]} et suit ces quelques principes
cite:{[}django\_design\_philosophies{]}:
\begin{itemize}
\item
Faible couplage et forte cohésion, pour que chaque composant dispose
de son indépendance, en n'ayant aucune connaissance des autres couches
applicatives. Ainsi, le moteur de rendu ne connait absolument rien
l'existence du moteur de base de données, tout comme le système de
vues ne sait pas quel moteur de rendu est utilisé.
\item
Plus de fonctionnalités avec moins de code: chaque application Django
doit utiliser le moins de code possible
\item
\emph{Don't repeat yourself}, chaque concept ou morceau de code ne
doit être présent qu'à un et un seul endroit de vos dépôts.
\item
Rapidité du développement, en masquant les aspects fastidieux du
développement web actuel
\end{itemize}
Mis côte à côte, le suivi de ces principes permet une bonne stabilité du
projet à moyen et long terme.
Comme nous le verrons par la suite, et sans être parfait, Django offre
une énorme flexibilité qui permet de se laisser le maximum d'options
ouvertes tout en permettant d'expérimenter facilement plusieurs pistes,
jusqu'au moment de prendre une vraie décision. Dans la majorité des cas
problématiques pouvant être rencontrés lors du développement d'une
application Web, Django proposera une solution pragmatique,
compréhensible et facile à mettre en place. En résumé de ce paragraphe,
pour tout problème commun, vous disposerez d'une solution logique. Tout
pour plaire à n'importe quel directeur IT.
\textbf{Dans la première partie}, nous verrons comment partir d'un
environnement sain, comment le configurer correctement, comment
installer Django de manière isolée et comment démarrer un nouveau
projet. Nous verrons rapidement comment gérer les dépendances, les
versions et comment appliquer et suivre un score de qualité de notre
code. Ces quelques points pourront être appliqués pour n'importe quel
langage ou cadre de travail. Nous verrons aussi que la configuration
proposée par défaut par le framework n'est pas idéale dans la majorité
des cas.
Pour cela, nous présenterons différents outils, la rédaction de tests
unitaires et d'intégration pour limiter les régressions, les règles de
nomenclature et de contrôle du contenu, comment partir d'un squelette
plus complet, ainsi que les bonnes étapes à suivre pour arriver à un
déploiement rapide et fonctionnel avec peu d'efforts.
A la fin de cette partie, vous disposerez d'un code propre et d'un
projet fonctionnel, bien qu'encore un peu inutile.
\textbf{Dans la deuxième partie}, nous aborderons les grands principes
de modélisation, en suivant les lignes de conduites du cadre de travail.
Nous aborderons les concepts clés qui permettent à une application de
rester maintenable, les formulaires, leurs validations, comment gérer
les données en entrée, les migrations de données et l'administration.
\textbf{Dans la troisième partie}, nous détaillerons précisément les
étapes de déploiement, avec la description et la configuration de
l'infrastructure, des exemples concrets de mise à disposition sur deux
distributions principales (Debian et CentOS), sur une \emph{*Plateform
as a Service*}, ainsi que l'utilisation de Docker et Docker-Compose.
Nous aborderons également la supervision et la mise à jour d'une
application existante, en respectant les bonnes pratiques
d'administration système.
\textbf{Dans la quatrième partie}, nous aborderons les architectures
typées \emph{entreprise}, les services et les différentes manières de
structurer notre application pour faciliter sa gestion et sa
maintenance, tout en décrivant différents types de scénarii, en fonction
des consommateurs de données.
\textbf{Dans la cinquième partie}, nous mettrons ces concepts en
pratique en présentant le développement en pratique de deux
applications, avec la description de problèmes rencontrés et la solution
qui a été choisie: définition des tables, gestion des utilisateurs,
\ldots\hspace{0pt} et mise à disposition.
\hypertarget{_pour_qui}{%
\subsection{Pour qui ?}\label{_pour_qui}}
Avant tout, pour moi. Comme le disait le Pr Richard Feynman: "\emph{Si
vous ne savez pas expliquer quelque chose simplement, c'est que vous ne
l'avez pas compris}". \footnote{Et comme l'ajoutait Aurélie Jean dans de
L'autre côté de la machine: \emph{"Si personne ne vous pose de
questions suite à votre explication, c'est que vous n'avez pas été
suffisamment clair !"} cite:{[}other\_side}{]}
Ce livre s'adresse autant au néophyte qui souhaite se lancer dans le
développement Web qu'à l'artisan qui a besoin d'un aide-mémoire et qui
ne se rappelle plus toujours du bon ordre des paramètres, ou à l'expert
qui souhaiterait avoir un aperçu d'une autre technologie que son domaine
privilégié de compétences.
Beaucoup de concepts présentés peuvent être oubliés ou restés inconnus
jusqu'au moment où ils seront réellement nécessaires. A ce moment-là,
pour peu que votre mémoire ait déjà entraperçu le terme, il vous sera
plus facile d'y revenir et de l'appliquer.
\hypertarget{_pour_aller_plus_loin}{%
\subsection{Pour aller plus loin}\label{_pour_aller_plus_loin}}
Il existe énormément de ressources, autant spécifiques à Django que plus
généralistes. Il ne sera pas possible de toutes les détailler; faites un
tour sur
\begin{itemize}
\item
\url{https://duckduckgo.com},
\item
\url{https://stackoverflow.com},
\item
\url{https://ycombinator.com},
\item
\url{https://lobste.rs/},
\item
\url{https://lecourrierduhacker.com/}
\item
ou \url{https://www.djangoproject.com/}.
\end{itemize}
Restez curieux, ne vous enclavez pas dans une technologie en particulier
et gardez une bonne ouverture d'esprit.
\hypertarget{_conventions}{%
\subsection{Conventions}\label{_conventions}}
Les notes indiquent des anecdotes.
Les conseils indiquent des éléments utiles, mais pas spécialement
indispensables.
Les notes importantes indiquent des éléments à retenir.
Ces éléments indiquent des points d'attention. Les retenir vous fera
gagner du temps en débuggage.
Les avertissements indiquent un (potentiel) danger ou des éléments
pouvant amener des conséquences pas spécialement sympathiques.
Les morceaux de code source seront présentés de la manière suivante:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# \textless{}folder\textgreater{}/\textless{}fichier\textgreater{}.\textless{}extension\textgreater{} }
\KeywordTok{def}\NormalTok{ function(param):}
\CommentTok{""" }
\CommentTok{ """}
\NormalTok{ callback() }
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
L'emplacement du fichier ou du morceau de code présenté, sous forme de
commentaire
\item
Des commentaires, si cela s'avère nécessaire
\item
Les parties importantes ou récemment modifiées seront surlignées.
\end{itemize}
La plupart des commandes qui seront présentées dans ce livre le seront
depuis un shell sous GNU/Linux. Certaines d'entre elles pourraient
devoir être adaptées si vous utilisez un autre système d'exploitation
(macOS) ou n'importe quelle autre grosse bouse commerciale.
\begin{quote}
Make it work, make it right, make it fast
--- Kent Beck
\end{quote}
En fonction de vos connaissances et compétences, la création d'une
nouvelle application est uneé tape relativement facile à mettre en
place. Le code qui permet de faire tourner cette application peut ne pas
être élégant, voire buggé jusqu'à la moëlle, il pourra fonctionner et
faire "preuve de concept".
Les problèmes arriveront lorsqu'une nouvelle demande sera introduite,
lorsqu'un bug sera découvert et devra être corrigé ou lorsqu'une
dépendance cessera de fonctionner ou d'être disponible. Or, une
application qui n'évolue pas, meurt. Tout application est donc destinée,
soit à être modifiée, corrigée et suivie, soit à déperrir et à être
délaissée par ses utilisateurs. Et c'est juste cette maintenance qui est
difficile.
L'application des principes présentés et agrégés ci-dessous permet
surtout de préparer correctement tout ce qui pourra arriver, sans aller
jusqu'au « \textbf{You Ain't Gonna Need It} » (ou \textbf{YAGNI}), qui
consiste à surcharger tout développement avec des fonctionnalités non
demandées, juste « au cas ou ». Pour paraphraser une partie de
l'introduction du livre \emph{Clean Architecture}
cite:{[}clean\_architecture{]}:
\begin{quote}
Getting software right is hard: it takes knowledge and skills that most
young programmers don't take the time to develop. It requires a level of
discipline and dedication that most programmers never dreamed they'd
need. Mostly, it takes a passion for the craft and the desire to be a
professional.
--- Robert C. Martin Clean Architecture
\end{quote}
Le développement d'un logiciel nécessite une rigueur d'exécution et des
connaissances précises dans des domaines extrêmement variés. Il
nécessite également des intentions, des (bonnes) décisions et énormément
d'attention. Indépendamment de l'architecture que vous aurez choisie,
des technologies que vous aurez patiemment évaluées et mises en place,
une architecture et une solution peuvent être cassées en un instant, en
même temps que tout ce que vous aurez construit, dès que vous en aurez
détourné le regard.
Un des objectifs ici est de placer les barrières et les gardes-fous (ou
plutôt, les "\textbf{garde-vous}"), afin de péréniser au maximum les
acquis, stabiliser les bases de tous les environnements (du
développement à la production) qui accueilliront notre application et
fiabiliser ainsi chaque étape de communication.
Dans cette partie-ci, nous parlerons de \textbf{méthodes de travail},
avec comme objectif d'éviter que l'application ne tourne que sur notre
machine et que chaque déploiement ne soit une plaie à gérer. Chaque mise
à jour doit être réalisable de la manière la plus simple possible, et
chaque étape doit être rendue la plus automatisée/automatisable
possible. Dans son plus simple élément, une application pourrait être
mise à jour simplement en envoyant son code sur un dépôt centralisé: ce
déclencheur doit démarrer une chaîne de vérification
d'utilisabilité/fonctionnalités/débuggabilité/sécurité, pour
immédiatement la mettre à disposition de nouveaux utilisateurs si toute
la chaîne indique que tout est OK. D'autres mécanismes fonctionnent
également, mais au plus les actions nécessitent d'actions humaines,
voire d'intervenants humains, au plus la probabilité qu'un problème
survienne est grande.
Dans une version plus manuelle, cela pourrait se résumer à ces trois
étapes (la dernière étant formellement facultative):
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Démarrer un script,
\item
Prévoir un rollback si cela plante (et si cela a planté, préparer un
post-mortem de l'incident pour qu'il ne se produise plus)
\item
Se préparer une tisane en regardant nos flux RSS (pour peu que cette
technologie existe encore\ldots\hspace{0pt}).
\end{enumerate}
\hypertarget{_pouxe9sie_de_la_programmation}{%
\section{Poésie de la
programmation}\label{_pouxe9sie_de_la_programmation}}
\hypertarget{_complexituxe9_de_mccabe}{%
\subsection{Complexité de McCabe}\label{_complexituxe9_de_mccabe}}
La \href{https://fr.wikipedia.org/wiki/Nombre_cyclomatique}{complexité
cyclomatique} (ou complexité de McCabe) peut s'apparenter à mesure de
difficulté de compréhension du code, en fonction du nombre
d'embranchements trouvés dans une même section. Quand le cycle
d'exécution du code rencontre une condition, il peut soit rentrer
dedans, soit passer directement à la suite.
Par exemple:
\begin{Shaded}
\begin{Highlighting}[]
\ControlFlowTok{if} \VariableTok{True} \OperatorTok{==} \VariableTok{False}\NormalTok{:}
\ControlFlowTok{pass} \CommentTok{\# never happens}
\CommentTok{\# continue ...}
\end{Highlighting}
\end{Shaded}
TODO: faut vraiment reprendre un cas un peu plus lisible. Là, c'est
naze.
La condition existe, mais nous ne passerons jamais dedans. A l'inverse,
le code suivant aura une complexité moisie à cause du nombre de
conditions imbriquées:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ compare(a, b, c, d, e):}
\ControlFlowTok{if}\NormalTok{ a }\OperatorTok{==}\NormalTok{ b:}
\ControlFlowTok{if}\NormalTok{ b }\OperatorTok{==}\NormalTok{ c:}
\ControlFlowTok{if}\NormalTok{ c }\OperatorTok{==}\NormalTok{ d:}
\ControlFlowTok{if}\NormalTok{ d }\OperatorTok{==}\NormalTok{ e:}
\BuiltInTok{print}\NormalTok{(}\StringTok{\textquotesingle{}Yeah!\textquotesingle{}}\NormalTok{)}
\ControlFlowTok{return} \DecValTok{1}
\end{Highlighting}
\end{Shaded}
Potentiellement, les tests unitaires qui seront nécessaires à couvrir
tous les cas de figure seront au nombre de cinq:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
le cas par défaut (a est différent de b, rien ne se passe),
\item
le cas où \texttt{a} est égal à \texttt{b}, mais où \texttt{b} est
différent de \texttt{c}
\item
le cas où \texttt{a} est égal à \texttt{b}, \texttt{b} est égal à
\texttt{c}, mais \texttt{c} est différent de \texttt{d}
\item
le cas où \texttt{a} est égal à \texttt{b}, \texttt{b} est égal à
\texttt{c}, \texttt{c} est égal à \texttt{d}, mais \texttt{d} est
différent de \texttt{e}
\item
le cas où \texttt{a} est égal à \texttt{b}, \texttt{b} est égal à
\texttt{c}, \texttt{c} est égal à \texttt{d} et \texttt{d} est égal à
\texttt{e}
\end{enumerate}
La complexité cyclomatique d'un bloc est évaluée sur base du nombre
d'embranchements possibles; par défaut, sa valeur est de 1. Si nous
rencontrons une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, nous allons devoir vérifier au moins chacun
des cas pour nous assurer que la couverture est complète. Nous devrions
donc trouver:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Un test où rien de se passe (\texttt{a\ !=\ b})
\item
Un test pour entrer dans la condition \texttt{a\ ==\ b}
\item
Un test pour entrer dans la condition \texttt{b\ ==\ c}
\item
Un test pour entrer dans la condition \texttt{c\ ==\ d}
\item
Un test pour entrer dans la condition \texttt{d\ ==\ e}
\end{enumerate}
Nous avons donc bien besoin de minimum cinq tests pour couvrir
l'entièreté des cas présentés.
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. Certains
recommandent de le garder sous une complexité de 10; d'autres de 5.
Il est important de noter que refactoriser un bloc pour en extraire une
méthode n'améliorera pas la complexité cyclomatique globale de
l'application. Nous visons ici une amélioration \textbf{locale}.
\hypertarget{_conclusion}{%
\subsection{Conclusion}\label{_conclusion}}
\begin{quote}
The primary cost of maintenance is in spelunking and risk
cite:{[}clean\_architecture(139){]}
--- Robert C. Martin
\end{quote}
En ayant connaissance de toutes les choses qui pourraient être modifiées
par la suite, l'idée est de pousser le développement jusqu'au point où
un service pourrait être nécessaire. A ce stade, l'architecture
nécessitera des modifications, mais aura déjà intégré le fait que cette
possibilité existe. Nous n'allons donc pas jusqu'au point où le service
doit être créé (même s'il peut ne pas être nécessaire), ni à l'extrême
au fait d'ignorer qu'un service pourrait être nécessaire, mais nous
aboutissons à une forme de compromis. Une forme de comportement de
Descartes, qui ne croit pas en Dieu, mais qui envisage quand même cette
possibilité, ce qui lui ouvre le maximum de portes 🙃
Avec cette approche, les composants sont déjà découplés au niveau du
code source, ce qui pourrait s'avérer suffisant jusqu'au stade où une
modification ne pourra plus faire reculer l'échéance.
En terme de découpe, les composants peuvent l'être aux niveaux suivants:
\begin{itemize}
\item
Code source
\item
Déploiement, au travers de dll, jar, linked libraries, \ldots{} voire
au travers de threads ou de processus locaux.
\item
Services
\end{itemize}
Cette section se base sur deux ressources principales
cite:{[}maintainable\_software{]} cite:{[}clean\_code{]}, qui
répartissent un ensemble de conseils parmi quatre niveaux de composants:
\begin{itemize}
\item
Les méthodes et fonctions
\item
Les classes
\item
Les composants
\item
Et des conseils plus généraux.
\end{itemize}
Ces conseils sont valables pour n'importe quel langage.
\hypertarget{_au_niveau_des_muxe9thodes_et_fonctions}{%
\subsubsection{Au niveau des méthodes et
fonctions}\label{_au_niveau_des_muxe9thodes_et_fonctions}}
\begin{itemize}
\item
\textbf{Gardez vos méthodes/fonctions courtes}. Pas plus de 15 lignes,
en comptant les commentaires. Des exceptions sont possibles, mais dans
une certaine mesure uniquement (pas plus de 6.9\% de plus de 60
lignes; pas plus de 22.3\% de plus de 30 lignes, au plus 43.7\% de
plus de 15 lignes et au moins 56.3\% en dessous de 15 lignes). Oui,
c'est dur à tenir, mais faisable.
\item
\textbf{Conserver une complexité de McCabe en dessous de 5},
c'est-à-dire avec quatre branches au maximum. A nouveau, si une
méthode présente une complexité cyclomatique de 15, la séparer en 3
fonctions ayant chacune une complexité de 5 conservera la complexité
globale à 15, mais rendra le code de chacune de ces méthodes plus
lisible, plus maintenable.
\item
\textbf{N'écrivez votre code qu'une seule fois: évitez les
duplications, copie, etc.}, c'est juste mal: imaginez qu'un bug soit
découvert dans une fonction; il devra alors être corrigé dans toutes
les fonctions qui auront été copiées/collées. C'est aussi une forme de
régression.
\item
\textbf{Conservez de petites interfaces}. Quatre paramètres, pas plus.
Au besoin, refactorisez certains paramètres dans une classe ou une
structure, qui sera plus facile à tester.
\end{itemize}
\hypertarget{_au_niveau_des_classes}{%
\subsubsection{Au niveau des classes}\label{_au_niveau_des_classes}}
\begin{itemize}
\item
\textbf{Privilégiez un couplage faible entre vos classes}. Ceci n'est
pas toujours possible, mais dans la mesure du possible, éclatez vos
classes en fonction de leur domaine de compétences respectif.
L'implémentation du service \texttt{UserNotificationsService} ne doit
pas forcément se trouver embarqué dans une classe
\texttt{UserService}. De même, pensez à passer par une interface
(commune à plusieurs classes), afin d'ajouter une couche
d'abstraction. La classe appellante n'aura alors que les méthodes
offertes par l'interface comme points d'entrée.
\end{itemize}
\hypertarget{_au_niveau_des_composants}{%
\subsubsection{Au niveau des
composants}\label{_au_niveau_des_composants}}
\begin{itemize}
\item
\textbf{Tout comme pour les classes, il faut conserver un couplage
faible au niveau des composants} également. Une manière d'arriver à ce
résultat est de conserver un nombre de points d'entrée restreint, et
d'éviter qu'il ne soit possible de contacter trop facilement des
couches séparées de l'architecture. Pour une architecture n-tiers par
exemple, la couche d'abstraction à la base de données ne peut être
connue que des services; sans cela, au bout de quelques semaines,
n'importe quelle couche de présentation risque de contacter
directement la base de données, "\emph{juste parce qu'elle en a la
possibilité}". Vous pourriez également passer par des interfaces, afin
de réduire le nombre de points d'entrée connus par un composant
externe (qui ne connaîtra par exemple que \texttt{IFileTransfer} avec
ses méthodes \texttt{put} et \texttt{get}, et non pas les détails
d'implémentation complet d'une classe \texttt{FtpFileTransfer} ou
\texttt{SshFileTransfer}).
\item
\textbf{Conserver un bon balancement au niveau des composants}: évitez
qu'un composant \textbf{A} ne soit un énorme mastodonte, alors que le
composant juste à côté ne soit capable que d'une action. De cette
manière, les nouvelles fonctionnalités seront mieux réparties parmi
les différents systèmes, et les responsabilités seront plus faciles à
gérer. Un conseil est d'avoir un nombre de composants compris entre 6
et 12 (idéalement, 12), et que chacun de ces composants soit
approximativement de même taille.
\end{itemize}
\hypertarget{_de_maniuxe8re_plus_guxe9nuxe9rale}{%
\subsubsection{De manière plus
générale}\label{_de_maniuxe8re_plus_guxe9nuxe9rale}}
\begin{itemize}
\item
\textbf{Conserver une densité de code faible}: il n'est évidemment pas
possible d'implémenter n'importe quelle nouvelle fonctionnalité en
moins de 20 lignes de code; l'idée ici est que la réécriture du projet
ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement)
passer du temps à réduire la taille du code existant: soit en faisant
du refactoring (intensif?), soit en utilisant des librairies
existantes, soit en explosant un système existant en plusieurs
sous-systèmes communiquant entre eux. Mais surtout, en évitant de
copier/coller bêtement du code existant.
\item
\textbf{Automatiser les tests}, \textbf{ajouter un environnement
d'intégration continue dès le début du projet} et \textbf{vérifier par
des outils les points ci-dessus}.
\end{itemize}
Unresolved directive in part-1-workspace/\_main.adoc -
include::mccabe.adoc{[}{]}
\hypertarget{_fiabilituxe9_uxe9volutivituxe9_et_maintenabilituxe9}{%
\section{Fiabilité, évolutivité et
maintenabilité}\label{_fiabilituxe9_uxe9volutivituxe9_et_maintenabilituxe9}}
\hypertarget{_12_facteurs}{%
\subsection{12 facteurs}\label{_12_facteurs}}
Pour la méthode de travail et de développement, nous allons nous baser
sur les \href{https://12factor.net/fr/}{The Twelve-factor App} - ou plus
simplement les \textbf{12 facteurs}.
L'idée derrière cette méthode, et indépendamment des langages de
développement utilisés, consiste à suivre un ensemble de douze concepts,
afin de:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
\textbf{Faciliter la mise en place de phases d'automatisation}; plus
concrètement, de faciliter les mises à jour applicatives, simplifier
la gestion de l'hôte, diminuer la divergence entre les différents
environnements d'exécution et offrir la possibilité d'intégrer le
projet dans un processus
d'\href{https://en.wikipedia.org/wiki/Continuous_integration}{intégration
continue} ou
\href{https://en.wikipedia.org/wiki/Continuous_deployment}{déploiement
continu}
\item
\textbf{Faciliter la mise à pied de nouveaux développeurs ou de
personnes souhaitant rejoindre le projet}, dans la mesure où la mise à
disposition d'un environnement sera grandement facilitée.
\item
\textbf{Minimiser les divergences entre les différents environnemens
sur lesquels un projet pourrait être déployé}
\item
\textbf{Augmenter l'agilité générale du projet}, en permettant une
meilleure évolutivité architecturale et une meilleure mise à l'échelle
- \emph{Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on
n'en parle plus ;-)}.
\end{enumerate}
En pratique, les points ci-dessus permettront de monter facilement un
nouvel environnement - qu'il soit sur la machine du petit nouveau dans
l'équipe, sur un serveur Azure/Heroku/Digital Ocean ou votre nouveau
Raspberry Pi Zéro caché à la cave - et vous feront gagner un temps
précieux.
Pour reprendre de manière très brute les différentes idées derrière
cette méthode, nous avons:
\textbf{\#1 - Une base de code unique, suivie par un système de contrôle
de versions}.
Chaque déploiement de l'application se basera sur cette source, afin de
minimiser les différences que l'on pourrait trouver entre deux
environnements d'un même projet. On utilisera un dépôt Git - Github,
Gitlab, Gitea, \ldots\hspace{0pt} Au choix.
\includegraphics{images/diagrams/12-factors-1.png}
Comme l'explique Eran Messeri, ingénieur dans le groupe Google Developer
Infrastructure, un des avantages d'utiliser un dépôt unique de sources,
est qu'il permet un accès facile et rapide à la forme la plus à jour du
code, sans aucun besoin de coordination. \footnote{The DevOps Handbook,
Part V, Chapitre 20, Convert Local Discoveries into Global
Improvements} Ce dépôt ne sert pas seulement au code source, mais
également à d'autres artefacts et formes de connaissance:
\begin{itemize}
\item
Standards de configuration (Chef recipes, Puppet manifests,
\ldots\hspace{0pt})
\item
Outils de déploiement
\item
Standards de tests, y compris tout ce qui touche à la sécurité
\item
Outils de déploiement de pipeline
\item
Outils d'analyse et de monitoring
\item
Tutoriaux
\end{itemize}
\textbf{\#2 - Déclarez explicitement les dépendances nécessaires au
projet, et les isoler du reste du système lors de leur installation}
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.
Cela permet d'éviter que l'application n'utilise une dépendance qui 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 notre cas, 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}.
Mais dans tous les cas, chaque application doit disposer d'un
environnement sain, qui lui est assigné, et vu le peu de ressources que
cela coûte, il ne faut pas s'en priver.
Chaque dépendance pouvant être déclarée et épinglée dans un fichier, il
suffira de créer un nouvel environment vierge, puis d'utiliser ce
fichier comme paramètre pour installer les prérequis au bon
fonctionnement de notre application et vérifier que cet environnement
est bien reproductible.
Il est important de bien "épingler" les versions liées aux dépendances
de l'application. Cela peut éviter des effets de bord comme une nouvelle
version d'une librairie dans laquelle un bug aurait pu avoir été
introduit. Parce qu'il arrive que ce genre de problème apparaisse, et
lorsque ce sera le cas, ce sera systématiquement au mauvais moment.
\textbf{\#3 - Sauver la configuration directement au niveau de
l'environnement}
Nous voulons éviter d'avoir à recompiler/redéployer l'application parce
que:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
l'adresse du serveur de messagerie a été modifiée,
\item
un protocole a changé en cours de route
\item
la base de données a été déplacée
\item
\ldots\hspace{0pt}
\end{enumerate}
En pratique, toute information susceptible de modifier un lien vers une
ressource annexe doit se trouver dans un fichier ou dans une variable
d'environnement, et doit être facilement modifiable. En allant un pas
plus loin, ceci 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, \ldots\hspace{0pt}) 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.
Au moment de développer une nouvelle fonctionnalité, réfléchissez si
l'un des composants utilisés risquerait de subir une modification: 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.
\textbf{\#4 - Traiter les ressources externes comme des ressources
attachées}
Nous parlons de bases de données, de services de mise en cache, d'API
externes, \ldots\hspace{0pt} 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, \ldots\hspace{0pt}) directement \emph{via} des
variables d'environnement.
\includegraphics{images/12factors/attached-resources.png}
Nous serons ravis de pouvoir simplement modifier une chaîne
\texttt{sqlite:////tmp/my-tmp-sqlite.db\textquotesingle{}} en
\texttt{psql://user:pass@127.0.0.1:8458/db} lorsque ce sera nécessaire,
sans avoir à recompiler ou redéployer les modifications.
\textbf{\#5 - Séparer proprement les phases de construction, de mise à
disposition et d'exécution}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
La \textbf{construction} (\emph{build}) convertit un code source en un
ensemble de fichiers exécutables, associé à une version et à une
transaction dans le système de gestion de sources.
\item
La \textbf{mise à disposition} (\emph{release}) associe cet ensemble à
une configuration prête à être exécutée,
\item
tandis que la phase d'\textbf{exécution} (\emph{run}) démarre les
processus nécessaires au bon fonctionnement de l'application.
\end{enumerate}
\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 ou sur
\href{https://fr.wikipedia.org/wiki/Capistrano_(logiciel)}{Capistrano}.
\textbf{\#6 - Les processus d'exécution ne doivent rien connaître ou
conserver de l'état de l'application}
Toute information stockée en mémoire ou sur disque ne doit pas altérer
le comportement futur de l'application, par exemple après un redémarrage
non souhaité.
Pratiquement, si l'application devait rencontrer un problème, l'objectif
est de pouvoir la redémarrer rapidement sur un autre serveur (par
exemple suite à un problème matériel). Toute information qui aurait été
stockée durant l'exécution de l'application sur le premier hôte serait
donc perdue. Si une réinitialisation devait être nécessaire,
l'application ne devra pas compter sur la présence d'une information au
niveau du nouveau système. La solution consiste donc à jouer sur les
variables d'environnement (cf. \#3) et sur les informations que l'on
pourra trouver au niveau des ressources attachées (cf \#4).
Il serait également difficile d'appliquer une mise à l'échelle de
l'application, en ajoutant un nouveau serveur d'exécution, si une donnée
indispensable à son fonctionnement devait se trouver sur la seule
machine où elle est actuellement exécutée.
\textbf{\#7 - Autoriser la liaison d'un port de l'application à un port
du système hôte}
Les applications 12-factors sont auto-contenues et peuvent fonctionner
en autonomie totale. Elles doivent être joignables grâce à un mécanisme
de ponts, où l'hôte qui s'occupe de l'exécution effectue lui-même la
redirection vers l'un des ports ouverts par l'application, typiquement,
en HTTP ou via un autre protocole.
\includegraphics{images/diagrams/12-factors-7.png}
\textbf{\#8 - Faites confiance aux processus systèmes pour l'exécution
de l'application}
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 processus particuliers pour certaines tâches:
requêtes HTTP \emph{via} des processus Web; \emph{long-running} jobs
pour des processus asynchrones, \ldots\hspace{0pt} Si cela existe au
niveau du système, ne vous fatiguez pas: utilisez le.
\includegraphics{images/12factors/process-types.png}
\textbf{\#9 - Améliorer la robustesse de l'application grâce à des
arrêts élégants et à des démarrages rapides}
Par "arrêt élégant", nous voulons surtout éviter le
\texttt{kill\ -9\ \textless{}pid\textgreater{}} ou tout autre arrêt
brutal d'un processus qui nécessiterait une intervention urgente du
superviseur. De cette manière, les requêtes en cours peuvent se terminer
au mieux, tandis que le démarrage rapide de nouveaux processus
améliorera la balance d'un processus en cours d'extinction vers des
processus tout frais.
L'intégration de ces mécanismes dès les premières étapes de
développement limitera les perturbations et facilitera la prise en
compte d'arrêts inopinés (problème matériel, redémarrage du système
hôte, etc.).
\textbf{\#10 - Conserver les différents environnements aussi similaires
que possible, et limiter les divergences entre un environnement de
développement et de production}
L'exemple donné est un développeur qui utilise macOS, NGinx et SQLite,
tandis que l'environnement de production tourne sur une CentOS avec
Apache2 et PostgreSQL. Faire en sorte que tous les environnements soient
les plus similaires possibles limite les divergences entre
environnements, facilite les déploiements et limite la casse et la
découverte de modules non compatibles dès les premières phases de
développement.
Pour vous donner un exemple tout bête, SQLite utilise un
\href{https://www.sqlite.org/datatype3.html}{mécanisme de stockage
dynamique}, associée à la valeur plutôt qu'au schéma, \emph{via} un
système d'affinités. Un autre moteur de base de données définira un
schéma statique et rigide, où la valeur sera déterminée par son
contenant. Un champ \texttt{URLField} proposé par Django a une longeur
maximale par défaut de
\href{https://docs.djangoproject.com/en/3.1/ref/forms/fields/\#django.forms.URLField}{200
caractères}. Si vous faites vos développements sous SQLite et que vous
rencontrez une URL de plus de 200 caractères, votre développement sera
passera parfaitement bien, mais plantera en production (ou en
\emph{staging}, si vous faites les choses un peu mieux) parce que les
données seront tronquées\ldots\hspace{0pt}
Conserver des environements similaires limite ce genre de désagréments.
\textbf{\#11 - Gérer les journeaux d'évènements comme des flux}
Une application ne doit jamais se soucier de l'endroit où ses évènements
seront écrits, mais simplement de les envoyer sur la sortie
\texttt{stdout}. De cette manière, que nous soyons en développement sur
le poste d'un développeur avec une sortie console ou sur une machine de
production avec un envoi vers une instance
\href{https://www.graylog.org/}{Greylog} ou
\href{https://sentry.io/welcome/}{Sentry}, le routage des journaux sera
réalisé en fonction de sa nécessité et de sa criticité, et non pas parce
que le développeur l'a spécifié en dur dans son code. Cette phase est
critique, dans la mesure où les journaux d'exécution sont la seule
manière pour une application de communiquer son état vers l'extérieur:
recevoir une erreur interne de serveur est une chose; pouvoir obtenir un
minimum d'informations, voire un contexte de plantage complet en est une
autre.
\textbf{\#12 - Isoler les tâches administratives du reste de
l'application}
Evitez qu'une migration ne puisse être démarrée depuis une URL de
l'application, ou qu'un envoi massif de notifications ne soit accessible
pour n'importe quel utilisateur: les tâches administratives ne doivent
être accessibles qu'à un administrateur. Les applications 12facteurs
favorisent les langages qui mettent un environnement REPL (pour
\emph{Read}, \emph{Eval}, \emph{Print} et \emph{Loop}) à disposition (au
hasard: \href{https://pythonprogramminglanguage.com/repl/}{Python} ou
\href{https://kotlinlang.org/}{Kotlin}), ce qui facilite les étapes de
maintenance.
\hypertarget{_concevoir_pour_lopuxe9rationnel}{%
\subsection{Concevoir pour
l'opérationnel}\label{_concevoir_pour_lopuxe9rationnel}}
Une application devient nettement plus maintenable dès lors que l'équipe
de développement suit de près les différentes étapes de sa conception,
de la demande jusqu'à son aboutissement en production.
cite:{[}devops\_handbook(293-294){]} Au fur et à mesure que le code est
délibérément construit pour être maintenable, l'équipe gagne en
rapidité, en qualité et en fiabilité de déploiement, ce qui facilite les
tâches opérationnelles:
\begin{itemize}
\item
Activation d'une télémétrie suffisante dans les applications et les
environnements.
\item
Conservation précise des dépendances nécessaires
\item
Résilience des services et plantage élégant (i.e. \textbf{sans finir
sur un SEGFAULT avec l'OS dans les choux et un écran bleu})
\item
Compatibilité entre les différentes versions (n+1, \ldots\hspace{0pt})
\item
Gestion de l'espace de stockage associé à un environnement (pour
éviter d'avoir un environnement de production qui fait 157
Tera-octets)
\item
Activation de la recherche dans les logs
\item
Traces des requêtes provenant des utilisateurs, indépendamment des
services utilisés
\item
Centralisation de la configuration (\textbf{via} ZooKeeper, par
exemple)
\end{itemize}
\hypertarget{_robustesse_et_flexibilituxe9}{%
\subsection{Robustesse et
flexibilité}\label{_robustesse_et_flexibilituxe9}}
\begin{quote}
Un code mal pensé entraîne nécessairement une perte d'énergie et de
temps. Il est plus simple de réfléchir, au moment de la conception du
programme, à une architecture permettant une meilleure maintenabilité
que de devoir corriger un code "sale" \emph{a posteriori}. C'est pour
aider les développeurs à rester dans le droit chemin que les principes
SOLID ont été énumérés. cite:{[}gnu\_linux\_mag\_hs\_104{]}
\end{quote}
Les principes SOLID, introduit par Robert C. Martin dans les années 2000
sont les suivants:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
\textbf{SRP} - Single responsibility principle - Principe de
Responsabilité Unique
\item
\textbf{OCP} - Open-closed principle
\item
\textbf{LSP} - Liskov Substitution
\item
\textbf{ISP} - Interface ségrégation principle
\item
\textbf{DIP} - Dependency Inversion Principle
\end{enumerate}
En plus de ces principes de développement, il faut ajouter des principes
au niveau des composants, puis un niveau plus haut, au niveau, au niveau
architectural :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Reuse/release équivalence principle,
\item
Common Closure Principle,
\item
Common Reuse Principle.
\end{enumerate}
\hypertarget{_single_responsibility_principle}{%
\subsubsection{Single Responsibility
Principle}\label{_single_responsibility_principle}}
Le principe de responsabilité unique conseille de disposer de concepts
ou domaines d'activité qui ne s'occupent chacun que d'une et une seule
chose. Ceci rejoint (un peu) la
\href{https://en.wikipedia.org/wiki/Unix_philosophy}{Philosophie Unix},
documentée par Doug McIlroy et qui demande de "\emph{faire une seule
chose, mais le faire bien}" cite:{[}unix\_philosophy{]}. Une classe ou
un élément de programmtion ne doit donc pas avoir plus d'une raison de
changer.
Il est également possible d'étendre ce principe en fonction d'acteurs:
\begin{quote}
A module should be responsible to one and only one actor.
cite:{[}clean\_architecture{]}
--- Robert C. Martin
\end{quote}
Plutôt que de centraliser le maximum de code à un seul endroit ou dans
une seule classe par convenance ou commodité \footnote{Aussi appelé
\emph{God-Like object}}, le principe de responsabilité unique suggère
que chaque classe soit responsable d'un et un seul concept.
Une manière de voir les choses consiste à différencier les acteurs ou
les intervenants: imaginez disposer d'une classe représentant des
données de membres du personnel. Ces données pourraient être demandées
par trois acteurs, le CFO, le CTO et le COO. Ceux-ci ont tous besoin de
données et d'informations relatives à une même base de données
centralisées, mais ont chacun besoin d'une représentation différente ou
de traitements distincts. cite:{[}clean\_architecture{]}
Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux
employés, mais elles vont un cran plus loin et pourraient nécessiter des
ajustements spécifiques en fonction de l'acteur concerné et de la
manière dont il souhaite disposer des données. Dès que possible,
identifiez les différents acteurs et demandeurs, en vue de prévoir les
modifications qui pourraient être demandées par l'un d'entre eux.
Dans le cas d'un élément de code centralisé, une modification induite
par un des acteurs pourrait ainsi avoir un impact sur les données
utilisées par les autres.
Vous trouverez ci-dessous une classe \texttt{Document}, dont chaque
instance est représentée par trois propriétés: son titre, son contenu et
sa date de publication. Une méthode \texttt{render} permet également de
proposer (très grossièrement) un type de sortie et un format de contenu:
\texttt{XML} ou \texttt{Markdown}.
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Document:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, title, content, published\_at):}
\VariableTok{self}\NormalTok{.title }\OperatorTok{=}\NormalTok{ title}
\VariableTok{self}\NormalTok{.content }\OperatorTok{=}\NormalTok{ content}
\VariableTok{self}\NormalTok{.published\_at }\OperatorTok{=}\NormalTok{ published\_at}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, format\_type):}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"XML"}\NormalTok{:}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.title,}
\VariableTok{self}\NormalTok{.content,}
\VariableTok{self}\NormalTok{.published\_at.isoformat()}
\NormalTok{ )}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"Markdown"}\NormalTok{:}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(}\VariableTok{self}\NormalTok{.content)}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Format type \textquotesingle{}}\SpecialCharTok{\{\}}\StringTok{\textquotesingle{} is not known"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(format\_type))}
\end{Highlighting}
\end{Shaded}
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML,
\ldots\hspace{0pt}), il sera nécessaire de modifier la classe
\texttt{Document}, ce qui n'est ni intuitif (\emph{ce n'est pas le
document qui doit savoir dans quels formats il peut être envoyés}), ni
conseillé (\emph{lorsque nous aurons quinze formats différents à gérer,
il sera nécessaire d'avoir autant de conditions dans cette méthode}).
Une bonne pratique consiste à créer une nouvelle classe de rendu pour
chaque type de format à gérer:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Document:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, title, content, published\_at):}
\VariableTok{self}\NormalTok{.title }\OperatorTok{=}\NormalTok{ title}
\VariableTok{self}\NormalTok{.content }\OperatorTok{=}\NormalTok{ content}
\VariableTok{self}\NormalTok{.published\_at }\OperatorTok{=}\NormalTok{ published\_at}
\KeywordTok{class}\NormalTok{ DocumentRenderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"XML"}\NormalTok{:}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.title,}
\VariableTok{self}\NormalTok{.content,}
\VariableTok{self}\NormalTok{.published\_at.isoformat()}
\NormalTok{ )}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"Markdown"}\NormalTok{:}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(}\VariableTok{self}\NormalTok{.content)}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Format type \textquotesingle{}}\SpecialCharTok{\{\}}\StringTok{\textquotesingle{} is not known"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(format\_type))}
\end{Highlighting}
\end{Shaded}
A présent, lorsque nous devrons ajouter un nouveau format de prise en
charge, nous irons 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.
En prenant l'exemple d'une méthode qui communique avec une base de
données, ce ne sera pas à cette méthode à gérer l'inscription d'une
exception à un emplacement quelconque. Cette action doit être prise en
compte par une autre classe (ou un autre concept), 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.
Lorsque nous verrons les composants, le principe de responsabilité
unique deviendra le CCP - Common Closure Principle. Ensuite, lorsque
nous verrons l'architecture de l'application, ce sera la définition des
frontières (boundaries).
\hypertarget{_open_closed}{%
\subsubsection{Open Closed}\label{_open_closed}}
\begin{quote}
For software systems to be easy to change, they must be designed to
allow the behavior to change by adding new code instead of changing
existing code.
\end{quote}
L'objectif est de rendre le système facile à étendre, en évitant que
l'impact d'une modification ne soit trop grand.
Les exemples parlent d'eux-mêmes: des données doivent être présentées
dans une page web. Et demain, ce seras dans un document PDF. Et après
demain, ce sera dans un tableur Excel. La source de ces données restent
la même (au travers d'une couche de présentation), mais la mise en forme
diffère à chaque fois.
L'application n'a pas à connaître les détails d'implémentation: elle
doit juste permettre une forme d'extension, sans avoir à appliquer une
modification (ou une grosse modification) sur son cœur.
Un des principes essentiels en programmation orientée objets concerne
l'héritage de classes et la surcharge de méthodes: plutôt que de partir
sur une série de comparaisons pour définir le comportement d'une
instance, il est parfois préférable de définir une nouvelle sous-classe,
qui surcharge une méthode bien précise. Pour l'exemple, on pourrait
ainsi définir trois classes:
\begin{itemize}
\item
Une classe \texttt{Customer}, pour laquelle la méthode
\texttt{GetDiscount} ne renvoit rien;
\item
Une classe \texttt{SilverCustomer}, pour laquelle la méthode revoit
une réduction de 10\%;
\item
Une classe \texttt{GoldCustomer}, pour laquelle la même méthode
renvoit une réduction de 20\%.
\end{itemize}
Si nous rencontrons un nouveau type de client, il suffit de créer une
nouvelle sous-classe. Cela évite d'avoir à gérer un ensemble conséquent
de conditions dans la méthode initiale, en fonction d'une autre variable
(ici, le type de client).
Nous passerions ainsi de:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Customer():}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, customer\_type: }\BuiltInTok{str}\NormalTok{):}
\VariableTok{self}\NormalTok{.customer\_type }\OperatorTok{=}\NormalTok{ customer\_type}
\KeywordTok{def}\NormalTok{ get\_discount(customer: Customer) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{if}\NormalTok{ customer.customer\_type }\OperatorTok{==} \StringTok{"Silver"}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{10}
\ControlFlowTok{elif}\NormalTok{ customer.customer\_type }\OperatorTok{==} \StringTok{"Gold"}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{20}
\ControlFlowTok{return} \DecValTok{0}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack }\OperatorTok{=}\NormalTok{ Customer(}\StringTok{"Silver"}\NormalTok{)}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack.get\_discount()}
\DecValTok{10}
\end{Highlighting}
\end{Shaded}
A ceci:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Customer():}
\KeywordTok{def}\NormalTok{ get\_discount(}\VariableTok{self}\NormalTok{) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{0}
\KeywordTok{class}\NormalTok{ SilverCustomer(Customer):}
\KeywordTok{def}\NormalTok{ get\_discount(}\VariableTok{self}\NormalTok{) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{10}
\KeywordTok{class}\NormalTok{ GoldCustomer(Customer):}
\KeywordTok{def}\NormalTok{ get\_discount(}\VariableTok{self}\NormalTok{) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return} \DecValTok{20}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack }\OperatorTok{=}\NormalTok{ SilverCustomer()}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ jack.get\_discount()}
\DecValTok{10}
\end{Highlighting}
\end{Shaded}
En anglais, dans le texte : "\emph{Putting in simple words, the
``Customer'' class is now closed for any new modification but it's open
for extensions when new customer types are added to the project.}".
\textbf{En résumé}: nous fermons la classe \texttt{Customer} à toute
modification, mais nous ouvrons la possibilité de créer de nouvelles
extensions en ajoutant de nouveaux types {[}héritant de
\texttt{Customer}{]}.
De cette manière, nous simplifions également la maintenance de la
méthode \texttt{get\_discount}, dans la mesure où elle dépend
directement du type dans lequel elle est implémentée.
Nous pouvons également appliquer ceci à notre exemple sur les rendus de
document, où le code suivant:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ DocumentRenderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"XML"}\NormalTok{:}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\ControlFlowTok{if}\NormalTok{ format\_type }\OperatorTok{==} \StringTok{"Markdown"}\NormalTok{:}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Format type \textquotesingle{}}\SpecialCharTok{\{\}}\StringTok{\textquotesingle{} is not known"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(format\_type))}
\end{Highlighting}
\end{Shaded}
devient le suivant:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Renderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ControlFlowTok{raise} \PreprocessorTok{NotImplementedError}
\KeywordTok{class}\NormalTok{ XmlRenderer(Renderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document)}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\KeywordTok{class}\NormalTok{ MarkdownRenderer(Renderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\end{Highlighting}
\end{Shaded}
Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons
simplement une nouvelle classe de rendu qui héritera de
\texttt{Renderer}.
Ce point sera très utile lorsque nous aborderons les
\href{https://docs.djangoproject.com/en/stable/topics/db/models/\#proxy-models}{modèles
proxy}.
\hypertarget{_liskov_substitution}{%
\subsubsection{Liskov Substitution}\label{_liskov_substitution}}
Dans Clean Architecture, ce chapitre ci (le 9) est sans doute celui qui
est le moins complet. Je suis d'accord avec les exemples donnés, dans la
mesure où la définition concrète d'une classe doit dépendre d'une
interface correctement définie (et que donc, faire hériter un carré d'un
rectangle, n'est pas adéquat dans le mesure où cela induit l'utilisateur
en erreur), mais il y est aussi question de la définition d'un style
architectural pour une interface REST, mais sans donner de
solution\ldots\hspace{0pt}
Le principe de substitution fait qu'une classe héritant d'une autre
classe doit se comporter de la même manière que cette dernière. Il n'est
pas question que la sous-classe n'implémente pas certaines méthodes,
alors que celles-ci sont disponibles sa classe parente.
\begin{quote}
{[}\ldots\hspace{0pt}{]} if S is a subtype of T, then objects of type T
in a computer program may be replaced with objects of type S (i.e.,
objects of type S may be substituted for objects of type T), without
altering any of the desirable properties of that program (correctness,
task performed, etc.). (Source:
\href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia}).
\end{quote}
\begin{quote}
Let q(x) be a property provable about objects x of type T. Then q(y)
should be provable for objects y of type S, where S is a subtype of T.
(Source:
\href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia
aussi})
\end{quote}
Ce n'est donc pas parce qu'une classe \textbf{a besoin d'une méthode
définie dans une autre classe} qu'elle doit forcément en hériter. Cela
bousillerait le principe de substitution, dans la mesure où une instance
de cette classe pourra toujours être considérée comme étant du type de
son parent.
Petit exemple pratique: si nous définissons une méthode \texttt{walk} et
une méthode \texttt{eat} sur une classe \texttt{Duck}, et qu'une
réflexion avancée (et sans doute un peu alcoolisée) nous dit que
"\emph{Puisqu'un \texttt{Lion} marche aussi, faisons le hériter de notre
classe `Canard`"}, nous allons nous retrouver avec ceci:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Duck:}
\KeywordTok{def}\NormalTok{ walk(}\VariableTok{self}\NormalTok{):}
\BuiltInTok{print}\NormalTok{(}\StringTok{"Kwak"}\NormalTok{)}
\KeywordTok{def}\NormalTok{ eat(}\VariableTok{self}\NormalTok{, thing):}
\ControlFlowTok{if}\NormalTok{ thing }\KeywordTok{in}\NormalTok{ (}\StringTok{"plant"}\NormalTok{, }\StringTok{"insect"}\NormalTok{, }\StringTok{"seed"}\NormalTok{, }\StringTok{"seaweed"}\NormalTok{, }\StringTok{"fish"}\NormalTok{):}
\ControlFlowTok{return} \StringTok{"Yummy!"}
\ControlFlowTok{raise}\NormalTok{ IndigestionError(}\StringTok{"Arrrh"}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Lion(Duck):}
\KeywordTok{def}\NormalTok{ walk(}\VariableTok{self}\NormalTok{):}
\BuiltInTok{print}\NormalTok{(}\StringTok{"Roaaar!"}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
Le principe de substitution de Liskov suggère qu'une classe doit
toujours pouvoir être considérée comme une instance de sa classe parent,
et \textbf{doit pouvoir s'y substituer}. Dans notre exemple, cela
signifie que nous pourrons tout à fait accepter qu'un lion se comporte
comme un canard et adore manger des plantes, insectes, graines, algues
et du poisson. Miam ! Nous vous laissons tester la structure ci-dessus
en glissant une antilope dans la boite à goûter du lion, ce qui nous
donnera quelques trucs bizarres (et un lion atteint de botulisme).
Pour revenir à nos exemples de rendus de documents, nous aurions pu
faire hériter notre \texttt{MarkdownRenderer} de la classe
\texttt{XmlRenderer}:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ XmlRenderer:}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document)}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\KeywordTok{class}\NormalTok{ MarkdownRenderer(XmlRenderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\end{Highlighting}
\end{Shaded}
Mais lorsque nous ajouterons une fonction d'entête, notre rendu en
Markdown héritera irrémédiablement de cette même méthode:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ XmlRenderer:}
\KeywordTok{def}\NormalTok{ header(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return} \StringTok{"""\textless{}?xml version = "1.0"?\textgreater{}"""}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document)}
\ControlFlowTok{return} \StringTok{"""}\SpecialCharTok{\{\}}
\StringTok{ \textless{}document\textgreater{}}
\StringTok{ \textless{}title\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/title\textgreater{}}
\StringTok{ \textless{}content\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/content\textgreater{}}
\StringTok{ \textless{}publication\_date\textgreater{}}\SpecialCharTok{\{\}}\StringTok{\textless{}/publication\_date\textgreater{}}
\StringTok{ \textless{}/document\textgreater{}"""}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.header(),}
\NormalTok{ document.title,}
\NormalTok{ document.content,}
\NormalTok{ document.published\_at.isoformat()}
\NormalTok{ )}
\KeywordTok{class}\NormalTok{ MarkdownRenderer(XmlRenderer):}
\KeywordTok{def}\NormalTok{ render(}\VariableTok{self}\NormalTok{, document):}
\ImportTok{import}\NormalTok{ markdown}
\ControlFlowTok{return}\NormalTok{ markdown.markdown(document.content)}
\end{Highlighting}
\end{Shaded}
A nouveau, lorsque 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.
\hypertarget{_interface_segregation_principle}{%
\subsubsection{Interface Segregation
Principle}\label{_interface_segregation_principle}}
Le principe de ségrégation d'interface suggère de limiter la nécessité
de recompiler un module, en n'exposant que les opérations nécessaires à
l'exécution d'une classe. Ceci évite d'avoir à redéployer l'ensemble
d'une application.
\begin{quote}
The lesson here is that depending on something that carries baggage that
you don't need can cause you troubles that you didn't except.
\end{quote}
Ce principe stipule qu'un client ne doit pas dépendre d'une méthode dont
il n'a pas besoin. Plus simplement, plutôt que de dépendre d'une seule
et même (grosse) interface présentant un ensemble conséquent de
méthodes, il est proposé d'exploser cette interface en plusieurs (plus
petites) interfaces. Ceci permet aux différents consommateurs de
n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à
un besoin précis.
GNU/Linux Magazine cite:{[}gnu\_linux\_mag\_hs\_104(37-42){]} propose un
exemple d'interface permettant d'implémenter une imprimante:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{interface}\NormalTok{ IPrinter}
\NormalTok{\{}
\KeywordTok{public} \KeywordTok{abstract} \DataTypeTok{void} \FunctionTok{printPage}\NormalTok{();}
\KeywordTok{public} \KeywordTok{abstract} \DataTypeTok{void} \FunctionTok{scanPage}\NormalTok{();}
\KeywordTok{public} \KeywordTok{abstract} \DataTypeTok{void} \FunctionTok{faxPage}\NormalTok{();}
\NormalTok{\}}
\KeywordTok{public} \KeywordTok{class}\NormalTok{ Printer}
\NormalTok{\{}
\KeywordTok{protected}\NormalTok{ string name;}
\KeywordTok{public} \FunctionTok{Printer}\NormalTok{(string name)}
\NormalTok{ \{}
\KeywordTok{this}\NormalTok{.}\FunctionTok{name}\NormalTok{ = name;}
\NormalTok{ \}}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
L'implémentation d'une imprimante multifonction aura tout son sens:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{public} \KeywordTok{class}\NormalTok{ AllInOnePrinter }\KeywordTok{implements}\NormalTok{ Printer }\KeywordTok{extends}\NormalTok{ IPrinter}
\NormalTok{\{}
\KeywordTok{public} \FunctionTok{AllInOnePrinter}\NormalTok{(string name)}
\NormalTok{ \{}
\KeywordTok{super}\NormalTok{(name);}
\NormalTok{ \}}
\KeywordTok{public} \DataTypeTok{void} \FunctionTok{printPage}\NormalTok{()}
\NormalTok{ \{}
\BuiltInTok{System}\NormalTok{.}\FunctionTok{out}\NormalTok{.}\FunctionTok{println}\NormalTok{(}\KeywordTok{this}\NormalTok{.}\FunctionTok{name}\NormalTok{ + }\StringTok{": Impression"}\NormalTok{);}
\NormalTok{ \}}
\KeywordTok{public} \DataTypeTok{void} \FunctionTok{scanPage}\NormalTok{()}
\NormalTok{ \{}
\BuiltInTok{System}\NormalTok{.}\FunctionTok{out}\NormalTok{.}\FunctionTok{println}\NormalTok{(}\KeywordTok{this}\NormalTok{.}\FunctionTok{name}\NormalTok{ + }\StringTok{": Scan"}\NormalTok{);}
\NormalTok{ \}}
\KeywordTok{public} \DataTypeTok{void} \FunctionTok{faxPage}\NormalTok{()}
\NormalTok{ \{}
\BuiltInTok{System}\NormalTok{.}\FunctionTok{out}\NormalTok{.}\FunctionTok{println}\NormalTok{(}\KeywordTok{this}\NormalTok{.}\FunctionTok{name}\NormalTok{ + }\StringTok{": Fax"}\NormalTok{);}
\NormalTok{ \}}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
Tandis que l'implémentation d'une imprimante premier-prix ne servira pas
à grand chose:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{public} \KeywordTok{class}\NormalTok{ FirstPricePrinter }\KeywordTok{implements}\NormalTok{ Printer }\KeywordTok{extends}\NormalTok{ IPrinter}
\NormalTok{\{}
\KeywordTok{public} \FunctionTok{FirstPricePrinter}\NormalTok{(string name)}
\NormalTok{ \{}
\KeywordTok{super}\NormalTok{(name);}
\NormalTok{ \}}
\KeywordTok{public} \DataTypeTok{void} \FunctionTok{printPage}\NormalTok{()}
\NormalTok{ \{}
\BuiltInTok{System}\NormalTok{.}\FunctionTok{out}\NormalTok{.}\FunctionTok{println}\NormalTok{(}\KeywordTok{this}\NormalTok{.}\FunctionTok{name}\NormalTok{ + }\StringTok{": Impression"}\NormalTok{);}
\NormalTok{ \}}
\KeywordTok{public} \DataTypeTok{void} \FunctionTok{scanPage}\NormalTok{()}
\NormalTok{ \{}
\BuiltInTok{System}\NormalTok{.}\FunctionTok{out}\NormalTok{.}\FunctionTok{println}\NormalTok{(}\KeywordTok{this}\NormalTok{.}\FunctionTok{name}\NormalTok{ + }\StringTok{": Fonctionnalité absente"}\NormalTok{);}
\NormalTok{ \}}
\KeywordTok{public} \DataTypeTok{void} \FunctionTok{faxPage}\NormalTok{()}
\NormalTok{ \{}
\BuiltInTok{System}\NormalTok{.}\FunctionTok{out}\NormalTok{.}\FunctionTok{println}\NormalTok{(}\KeywordTok{this}\NormalTok{.}\FunctionTok{name}\NormalTok{ + }\StringTok{": Fonctionnalité absente"}\NormalTok{);}
\NormalTok{ \}}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
L'objectif est donc de découpler ces différentes fonctionnalités en
plusieurs interfaces bien spécifiques, implémentant chacune une
opération isolée:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{interface}\NormalTok{ IPrinterPrinter}
\NormalTok{\{}
\KeywordTok{public} \KeywordTok{abstract} \DataTypeTok{void} \FunctionTok{printPage}\NormalTok{();}
\NormalTok{\}}
\KeywordTok{interface}\NormalTok{ IPrinterScanner}
\NormalTok{\{}
\KeywordTok{public} \KeywordTok{abstract} \DataTypeTok{void} \FunctionTok{scanPage}\NormalTok{();}
\NormalTok{\}}
\KeywordTok{interface}\NormalTok{ IPrinterFax}
\NormalTok{\{}
\KeywordTok{public} \KeywordTok{abstract} \DataTypeTok{void} \FunctionTok{faxPage}\NormalTok{();}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
Cette réflexion s'applique finalement à n'importe quel composant: votre
système d'exploitation, les librairies et dépendances tierces, les
variables déclarées, \ldots\hspace{0pt} 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
En Python, ce comportement est inféré lors de l'exécution, et donc pas
vraiment d'application pour notre 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 mettre à jour une DLL ou un JAR sans que cela
n'ait d'impact sur le reste de l'application.
Il est ainsi possible de trouver quelques horreurs, dans tous les
langages:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{/*!}
\CommentTok{ * is{-}odd }\KeywordTok{\textless{}https:}\OtherTok{//github.com/jonschlinkert/is{-}odd}\KeywordTok{\textgreater{}}
\CommentTok{ *}
\CommentTok{ * Copyright (c) 2015{-}2017, Jon Schlinkert.}
\CommentTok{ * Released under the MIT License.}
\CommentTok{ */}
\StringTok{\textquotesingle{}use strict\textquotesingle{}}\OperatorTok{;}
\KeywordTok{const}\NormalTok{ isNumber }\OperatorTok{=} \PreprocessorTok{require}\NormalTok{(}\StringTok{\textquotesingle{}is{-}number\textquotesingle{}}\NormalTok{)}\OperatorTok{;}
\NormalTok{module}\OperatorTok{.}\AttributeTok{exports} \OperatorTok{=} \KeywordTok{function} \FunctionTok{isOdd}\NormalTok{(value) \{}
\KeywordTok{const}\NormalTok{ n }\OperatorTok{=} \BuiltInTok{Math}\OperatorTok{.}\FunctionTok{abs}\NormalTok{(value)}\OperatorTok{;}
\ControlFlowTok{if}\NormalTok{ (}\OperatorTok{!}\FunctionTok{isNumber}\NormalTok{(n)) \{}
\ControlFlowTok{throw} \KeywordTok{new} \BuiltInTok{TypeError}\NormalTok{(}\StringTok{\textquotesingle{}expected a number\textquotesingle{}}\NormalTok{)}\OperatorTok{;}
\NormalTok{ \}}
\ControlFlowTok{if}\NormalTok{ (}\OperatorTok{!}\BuiltInTok{Number}\OperatorTok{.}\FunctionTok{isInteger}\NormalTok{(n)) \{}
\ControlFlowTok{throw} \KeywordTok{new} \BuiltInTok{Error}\NormalTok{(}\StringTok{\textquotesingle{}expected an integer\textquotesingle{}}\NormalTok{)}\OperatorTok{;}
\NormalTok{ \}}
\ControlFlowTok{if}\NormalTok{ (}\OperatorTok{!}\BuiltInTok{Number}\OperatorTok{.}\FunctionTok{isSafeInteger}\NormalTok{(n)) \{}
\ControlFlowTok{throw} \KeywordTok{new} \BuiltInTok{Error}\NormalTok{(}\StringTok{\textquotesingle{}value exceeds maximum safe integer\textquotesingle{}}\NormalTok{)}\OperatorTok{;}
\NormalTok{ \}}
\ControlFlowTok{return}\NormalTok{ (n }\OperatorTok{\%} \DecValTok{2}\NormalTok{) }\OperatorTok{===} \DecValTok{1}\OperatorTok{;}
\NormalTok{\}}\OperatorTok{;}
\end{Highlighting}
\end{Shaded}
Voire, son opposé, qui dépend évidemment du premier:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{/*!}
\CommentTok{ * is{-}even }\KeywordTok{\textless{}https:}\OtherTok{//github.com/jonschlinkert/is{-}even}\KeywordTok{\textgreater{}}
\CommentTok{ *}
\CommentTok{ * Copyright (c) 2015, 2017, Jon Schlinkert.}
\CommentTok{ * Released under the MIT License.}
\CommentTok{ */}
\StringTok{\textquotesingle{}use strict\textquotesingle{}}\OperatorTok{;}
\KeywordTok{var}\NormalTok{ isOdd }\OperatorTok{=} \PreprocessorTok{require}\NormalTok{(}\StringTok{\textquotesingle{}is{-}odd\textquotesingle{}}\NormalTok{)}\OperatorTok{;}
\NormalTok{module}\OperatorTok{.}\AttributeTok{exports} \OperatorTok{=} \KeywordTok{function} \FunctionTok{isEven}\NormalTok{(i) \{}
\ControlFlowTok{return} \OperatorTok{!}\FunctionTok{isOdd}\NormalTok{(i)}\OperatorTok{;}
\NormalTok{\}}\OperatorTok{;}
\end{Highlighting}
\end{Shaded}
Il ne s'agit que d'un simple exemple, mais qui tend à une seule chose:
gardez les choses simples (et, éventuellement, stupides) kiss. Dans
l'exemple ci-dessus, l'utilisation du module \texttt{is-odd} requière
déjà deux dépendances: \texttt{is-even} et \texttt{is-number}. Imaginez
la suite.
\hypertarget{_dependency_inversion_principle}{%
\subsubsection{Dependency inversion
Principle}\label{_dependency_inversion_principle}}
Dans une architecture conventionnelle, les composants de haut-niveau
dépendent directement des composants de bas-niveau. L'inversion de
dépendances stipule que c'est le composant de haut-niveau qui possède la
définition de l'interface dont il a besoin, et le composant de
bas-niveau qui l'implémente. L'objectif est que les interfaces soient
les plus stables possibles, afin de réduire au maximum les modifications
qui pourraient y être appliquées. De cette manière, toute modification
fonctionnelle pourra être directement appliquée sur le composant de
bas-niveau, sans que l'interface ne soit impactée.
\begin{quote}
The dependency inversion principle tells us that the most flexible
systems are those in which source code dependencies refer only to
abstractions, not to concretions.
\end{quote}
cite:{[}clean\_architecture{]}
L'injection de dépendances est un patron de programmation qui suit le
principe d'inversion de dépendances.
Django est bourré de ce principe, que ce soit pour les
\emph{middlewares} ou pour les connexions aux bases de données. Lorsque
nous écrivons ceci dans notre fichier de configuration,
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# [snip]}
\NormalTok{MIDDLEWARE }\OperatorTok{=}\NormalTok{ [}
\StringTok{\textquotesingle{}django.middleware.security.SecurityMiddleware\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.sessions.middleware.SessionMiddleware\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.middleware.common.CommonMiddleware\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.middleware.csrf.CsrfViewMiddleware\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.auth.middleware.AuthenticationMiddleware\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.messages.middleware.MessageMiddleware\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.middleware.clickjacking.XFrameOptionsMiddleware\textquotesingle{}}\NormalTok{,}
\NormalTok{]}
\CommentTok{\# [snip]}
\end{Highlighting}
\end{Shaded}
Django ira simplement récupérer chacun de ces middlewares, qui répondent
chacun à une
\href{https://docs.djangoproject.com/en/4.0/topics/http/middleware/\#writing-your-own-middleware}{interface
clairement définie}, dans l'ordre. Il n'y a donc pas de magie; c'est le
développeur qui va simplement brancher ou câbler des fonctionnalités au
niveau du framework, en les déclarant au bon endroit. Pour créer un
nouveau \emph{middleware}, il suffira d'implémenter le code suivant et
de l'ajouter dans la configuration de l'application:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ simple\_middleware(get\_response):}
\CommentTok{\# One{-}time configuration and initialization.}
\KeywordTok{def}\NormalTok{ middleware(request):}
\CommentTok{\# Code to be executed for each request before}
\CommentTok{\# the view (and later middleware) are called.}
\NormalTok{ response }\OperatorTok{=}\NormalTok{ get\_response(request)}
\CommentTok{\# Code to be executed for each request/response after}
\CommentTok{\# the view is called.}
\ControlFlowTok{return}\NormalTok{ response}
\ControlFlowTok{return}\NormalTok{ middleware}
\end{Highlighting}
\end{Shaded}
Dans d'autres projets écrits en Python, ce type de mécanisme peut être
implémenté relativement facilement en utilisant les modules
\href{https://docs.python.org/3/library/importlib.html}{importlib} et la
fonction \texttt{getattr}.
Un autre exemple concerne les bases de données: pour garder un maximum
de flexibilité, Django ajoute une couche d'abstraction en permettant de
spécifier le moteur de base de données que vous souhaiteriez utiliser,
qu'il s'agisse d'SQLite, MSSQL, Oracle, PostgreSQL ou MySQL/MariaDB
\footnote{\url{http://howfuckedismydatabase.com/}}.
\begin{quote}
The database is really nothing more than a big bucket of bits where we
store our data on a long term basis.
\end{quote}
cite:{[}clean\_architecture(281){]}
D'un point de vue architectural, nous ne devons pas nous soucier de la
manière dont les données sont stockées, s'il s'agit d'un disque
magnétique, de ram, \ldots\hspace{0pt} en fait, on ne devrait même pas
savoir s'il y a un disque du tout. Et Django le fait très bien pour
nous.
En termes architecturaux, ce principe autorise une définition des
frontières, et en permettant une séparation claire en inversant le flux
de dépendances et en faisant en sorte que les règles métiers n'aient
aucune connaissance des interfaces graphiques qui les exploitent ou des
moteurs de bases de données qui les stockent. Ceci autorise une forme
d'immunité entre les composants.
\hypertarget{_sources}{%
\subsubsection{Sources}\label{_sources}}
\begin{itemize}
\item
\href{http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp}{Understanding
SOLID principles on CodeProject}
\item
\href{http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/}{Dependency
Injection is NOT the same as dependency inversion}
\item
\href{http://en.wikipedia.org/wiki/Dependency_injection}{Injection de
dépendances}
\end{itemize}
\hypertarget{_au_niveau_des_composants_2}{%
\subsection{au niveau des
composants}\label{_au_niveau_des_composants_2}}
De la même manière que pour les principes définis ci-dessus, Mais
toujours en faisant attention qu'une fois que les frontières sont
implémentés, elles sont coûteuses à maintenir. Cependant, il ne s'agit
pas une décision à réaliser une seule fois, puisque cela peut être
réévalué.
Et de la même manière que nous devons délayer au maximum les choix
architecturaux et techniques,
\begin{quote}
but this is not a one time decision. You don't simply decide at the
start of a project which boundaries to implémentent and which to ignore.
Rather, you watch. You pay attention as the system evolves. You note
where boundaries may be required, and then carefully watch for the first
inkling of friction because those boundaries don't exist. at that point,
you weight the costs of implementing those boundaries versus the cost of
ignoring them and you review that decision frequently. Your goal is to
implement the boundaries right at the inflection point where the cost of
implementing becomes less than the cost of ignoring.
\end{quote}
En gros, il faut projeter sur la capacité à s'adapter en minimisant la
maintenance. Le problème est qu'elle ne permettait aucune adaptation, et
qu'à la première demande, l'architecture se plante complètement sans
aucune malléabilité.
\hypertarget{_reuserelease_equivalence_principle}{%
\subsubsection{Reuse/release equivalence
principle}\label{_reuserelease_equivalence_principle}}
\begin{verbatim}
Classes and modules that are grouped together into a component should be releasable together
-- (Chapitre 13, Component Cohesion, page 105)
\end{verbatim}
\hypertarget{_ccp}{%
\subsubsection{CCP}\label{_ccp}}
(= l'équivalent du SRP, mais pour les composants)
\begin{quote}
If two classes are so tightly bound, either physically or conceptually,
that they always change together, then they belong in the same component
\end{quote}
Il y a peut-être aussi un lien à faire avec «~Your code as a crime
scene~» 🤟
\begin{verbatim}
La définition exacte devient celle-ci: « gather together those things that change at the same times and for the same reasons. Separate those things that change at different times or for different reasons ».
\end{verbatim}
\begin{verbatim}
==== CRP
\end{verbatim}
\begin{verbatim}
… que lon résumera ainsi: « dont depend on things you dont need » 😘
Au niveau des composants, au niveau architectural, mais également à dautres niveaux.
\end{verbatim}
\hypertarget{_sdp}{%
\subsubsection{SDP}\label{_sdp}}
(Stable dependency principle) qui définit une formule de stabilité pour
les composants, en fonction de sa faculté à être modifié et des
composants qui dépendent de lui: au plus un composant est nécessaire, au
plus il sera stable (dans la mesure où il lui sera difficile de
changer). En C++, cela correspond aux mots clés \#include. Pour
faciliter cette stabilité, il convient de passer par des interfaces
(donc, rarement modifiées, par définition).
En Python, ce ratio pourrait être calculé au travers des import, via les
AST.
\hypertarget{_sap}{%
\subsubsection{SAP}\label{_sap}}
(= Stable abstraction principle) pour la définition des politiques de
haut niveau vs les composants plus concrets. SAP est juste une
modélisation du OCP pour les composants: nous plaçons ceux qui ne
changent pas ou pratiquement pas le plus haut possible dans
l'organigramme (ou le diagramme), et ceux qui changent souvent plus bas,
dans le sens de stabilité du flux. Les composants les plus bas sont
considérés comme volatiles
\hypertarget{_architecture}{%
\section{Architecture}\label{_architecture}}
\begin{quote}
If you think good architecture is expensive, try bad architecture
--- Brian Foote and Joseph Yoder
\end{quote}
Au delà des principes dont il est question plus haut, c'est dans les
ressources proposées et les cas démontrés que l'on comprend leur
intérêt: plus que de la définition d'une architecture adéquate, c'est
surtout dans la facilité de maintenance d'une application que ces
principes s'identifient.
Une bonne architecture va rendre le système facile à lire, facile à
développer, facile à maintenir et facile à déployer. L'objectif ultime
étant de minimiser le coût de maintenance et de maximiser la
productivité des développeurs. Un des autres objectifs d'une bonne
architecture consiste également à se garder le plus d'options possibles,
et à se concentrer sur les détails (le type de base de données, la
conception concrète, \ldots\hspace{0pt}), le plus tard possible, tout en
conservant la politique principale en ligne de mire. Cela permet de
délayer les choix techniques à «~plus tard~», ce qui permet également de
concrétiser ces choix en ayant le plus d'informations possibles
cite:{[}clean\_architecture(137-141){]}
Derrière une bonne architecture, il y a aussi un investissement quant
aux ressources qui seront nécessaires à faire évoluer l'application: ne
pas investir dès qu'on le peut va juste lentement remplir la case de la
dette technique.
Une architecture ouverte et pouvant être étendue n'a d'intérêt que si le
développement est suivi et que les gestionnaires (et architectes)
s'engagent à économiser du temps et de la qualité lorsque des
changements seront demandés pour l'évolution du projet.
\hypertarget{_politiques_et_ruxe8gles_muxe9tiers}{%
\subsection{Politiques et règles
métiers}\label{_politiques_et_ruxe8gles_muxe9tiers}}
TODO: Un p'tit bout à ajouter sur les méthodes de conception ;)
\hypertarget{_considuxe9ration_sur_les_frameworks}{%
\subsection{Considération sur les
frameworks}\label{_considuxe9ration_sur_les_frameworks}}
\begin{quote}
Frameworks are tools to be used, not architectures to be conformed to.
Your architecture should tell readers about the system, not about the
frameworks you used in your system. If you are building a health care
system, then when new programmers look at the source repository, their
first impression should be, «~oh, this is a health care system~». Those
new programmers should be able to learn all the use cases of the system,
yet still not know how the system is delivered.
--- Robert C. Martin Clean Architecture
\end{quote}
Le point soulevé ci-dessous est qu'un framework n'est qu'un outil, et
pas une obligation de structuration. L'idée est que le framework doit se
conformer à la définition de l'application, et non l'inverse. Dans le
cadre de l'utilisation de Django, c'est un point critique à prendre en
considération: une fois que vous aurez fait ce choix, vous aurez
extrêmement difficile à faire machine arrière:
\begin{itemize}
\item
Votre modèle métier sera largement couplé avec le type de base de
données (relationnelle, indépendamment
\item
Votre couche de présentation sera surtout disponible au travers d'un
navigateur
\item
Les droits d'accès et permissions seront en grosse partie gérés par le
frameworks
\item
La sécurité dépendra de votre habilité à suivre les versions
\item
Et les fonctionnalités complémentaires (que vous n'aurez pas voulu/eu
le temps de développer) dépendront de la bonne volonté de la
communauté
\end{itemize}
Le point à comprendre ici n'est pas que "Django, c'est mal", mais qu'une
fois que vous aurez défini la politique, les règles métiers, les données
critiques et entités, et que vous aurez fait le choix de développer en
âme et conscience votre nouvelle création en utilisant Django, vous
serez bon gré mal gré, contraint de continuer avec. Cette décision ne
sera pas irrévocable, mais difficile à contourner.
\begin{quote}
At some point in their history most DevOps organizations were hobbled by
tightly-coupled, monolithic architectures that while extremely
successfull at helping them achieve product/market fit - put them at
risk of organizational failure once they had to operate at scale (e.g.
eBay's monolithic C++ application in 2001, Amazon's monolithic OBIDOS
application in 2001, Twitter's monolithic Rails front-end in 2009, and
LinkedIn's monolithic Leo application in 2011). In each of these cases,
they were able to re-architect their systems and set the stage not only
to survice, but also to thrise and win in the marketplace
\end{quote}
cite:{[}devops\_handbook(182){]}
Ceci dit, Django compense ses contraintes en proposant énormément de
flexibilité et de fonctionnalités \textbf{out-of-the-box}, c'est-à-dire
que vous pourrez sans doute avancer vite et bien jusqu'à un point de
rupture, puis revoir la conception et réinvestir à ce moment-là, mais en
toute connaissance de cause.
\begin{quote}
When any of the external parts of the system become obsolete, such as
the database, or the web framework, you can replace those obsolete
elements with a minimum of fuss.
--- Robert C. Martin Clean Architecture
\end{quote}
Avec Django, la difficulté à se passer du framework va consister à
basculer vers «~autre chose~» et a remplacer chacune des tentacules qui
aura pousser partout dans l'application.
A noter que les services et les «~architectures orientées services~» ne
sont jamais qu'une définition d'implémentation des frontières, dans la
mesure où un service n'est jamais qu'une fonction appelée au travers
d'un protocole (rest, soap, \ldots\hspace{0pt}). Une application
monolotihique sera tout aussi fonctionnelle qu'une application découpée
en microservices. (Services: great and small, page 243).
\hypertarget{_un_point_sur_linversion_de_duxe9pendances}{%
\subsection{Un point sur l'inversion de
dépendances}\label{_un_point_sur_linversion_de_duxe9pendances}}
Dans la partie SOLID, nous avons évoqué plusieurs principes de
développement. Django est un framework qui évolue, et qui a pu présenter
certains problèmes liés à l'un de ces principes.
Les link:release
notes{[}\url{https://docs.djangoproject.com/en/2.0/releases/2.0/}{]} de
Django 2.0 date de décembre 2017; parmi ces notes, l'une d'elles cite
l'abandon du support d'link:Oracle
11.2{[}\url{https://docs.djangoproject.com/en/2.0/releases/2.0/\#dropped-support-for-oracle-11-2}{]}.
En substance, cela signifie que le framework se chargeait lui-même de
construire certaines parties de requêtes, qui deviennent non
fonctionnelles dès lors que l'on met le framework ou le moteur de base
de données à jour. Réécrit, cela signifie que:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Si vos données sont stockées dans un moteur géré par Oracle 11.2, vous
serez limité à une version 1.11 de Django
\item
Tandis que si votre moteur est géré par une version ultérieure, le
framework pourra être mis à jour.
\end{enumerate}
Nous sommes dans un cas concret d'inversion de dépendances ratée: le
framework (et encore moins vos politiques et règles métiers) ne
devraient pas avoir connaissance du moteur de base de données. Pire, vos
politiques et données métiers ne devraient pas avoir connaissance
\textbf{de la version} du moteur de base de données.
En conclusion, le choix d'une version d'un moteur technique (\textbf{la
base de données}) a une incidence directe sur les fonctionnalités mises
à disposition par votre application, ce qui va à l'encontre des 12
facteurs (et des principes de développement).
Ce point sera rediscuté par la suite, notamment au niveau de l'épinglage
des versions, de la reproduction des environnements et de
l'interdépendance entre des choix techniques et fonctionnels.
\hypertarget{_tests_et_intuxe9gration}{%
\section{Tests et intégration}\label{_tests_et_intuxe9gration}}
\begin{quote}
Tests are part of the system. You can think of tests as the outermost
circle in the architecture. Nothing within in the system depends on the
tests, and the tests always depend inward on the components of the
system~».
--- Robert C. Martin Clean Architecture
\end{quote}
\hypertarget{_le_langage_python}{%
\section{Le langage Python}\label{_le_langage_python}}
Le langage \href{https://www.python.org/}{Python} est un
\href{https://docs.python.org/3/faq/general.html\#what-is-python}{langage
de programmation} interprété, interactif, amusant, orienté objet
(souvent), fonctionnel (parfois), open source, multi-plateformes,
flexible, facile à apprendre et difficile à maîtriser.
\begin{figure}
\centering
\includegraphics{images/xkcd-353-python.png}
\caption{\url{https://xkcd.com/353/}}
\end{figure}
A première vue, et suivants les autres langages que vous connaitriez ou
auriez déjà abordé, certains concepts restent difficiles à aborder:
l'indentation définit l'étendue d'un bloc (classe, fonction, méthode,
boucle, condition, \ldots\hspace{0pt}), il n'y a pas de typage fort des
variables et le compilateur n'est pas là pour assurer le filet de
sécurité avant la mise en production (puisqu'il n'y a pas de compilateur
😛). Et malgré ces quelques points, Python reste un langage généraliste
accessible et "bon partout", et de pouvoir se reposer sur un écosystème
stable et fonctionnel.
Il fonctionne avec un système d'améliorations basées sur des
propositions: les PEP, ou "\textbf{Python Enhancement Proposal}".
Chacune d'entre elles doit être approuvée par le
\href{http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life}{Benevolent
Dictator For Life}.
Le langage Python utilise un typage dynamique appelé
\href{https://fr.wikipedia.org/wiki/Duck_typing}{\textbf{duck typing}}:
"\emph{When I see a bird that quacks like a duck, walks like a duck, has
feathers and webbed feet and associates with ducks --- I'm certainly
going to assume that he is a duck}" Source:
\href{http://en.wikipedia.org/wiki/Duck_test}{Wikipedia}.
Les morceaux de code que vous trouverez ci-dessous seront développés
pour Python3.9+ et Django 3.2+. Ils nécessiteront peut-être quelques
adaptations pour fonctionner sur une version antérieure.
\hypertarget{_eluxe9ments_de_langage}{%
\subsection{Eléments de langage}\label{_eluxe9ments_de_langage}}
En fonction de votre niveau d'apprentissage du langage, plusieurs
ressources pourraient vous aider:
\begin{itemize}
\item
\textbf{Pour les débutants},
\href{https://automatetheboringstuff.com/}{Automate the Boring Stuff
with Python} cite:{[}boring\_stuff{]}, aka. \emph{Practical
Programming for Total Beginners}
\item
\textbf{Pour un (gros) niveau au dessus} et pour un état de l'art du
langage, nous ne pouvons que vous recommander le livre Expert Python
Programming cite:{[}expert\_python{]}, qui aborde énormément d'aspects
du langage en détails (mais pas toujours en profondeur): les
différents types d'interpréteurs, les éléments de langage avancés,
différents outils de productivité, métaprogrammation, optimisation de
code, programmation orientée évènements, multithreading et
concurrence, tests, \ldots\hspace{0pt} A ce jour, c'est le concentré
de sujets liés au langage le plus intéressant qui ait pu arriver entre
nos mains.
\end{itemize}
En parallèle, si vous avez besoin d'un aide-mémoire ou d'une liste
exhaustive des types et structures de données du langage, référez-vous
au lien suivant:
\href{https://gto76.github.io/python-cheatsheet/}{Python Cheat Sheet}.
\hypertarget{_protocoles_de_langage}{%
\subsubsection{Protocoles de langage}\label{_protocoles_de_langage}}
dunder
Le modèle de données du langage spécifie un ensemble de méthodes qui
peuvent être surchargées. Ces méthodes suivent une convention de nommage
et leur nom est toujours encadré par un double tiret souligné; d'où leur
nom de "\emph{dunder methods}" ou "\emph{double-underscore methods}". La
méthode la plus couramment utilisée est la méthode \texttt{init()}, qui
permet de surcharger l'initialisation d'une instance de classe.
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ CustomUserClass:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, initiatization\_argument):}
\NormalTok{ ...}
\end{Highlighting}
\end{Shaded}
cite:{[}expert\_python(142-144){]}
Ces méthodes, utilisées seules ou selon des combinaisons spécifiques,
constituent les \emph{protocoles de langage}. Une liste complètement des
\emph{dunder methods} peut être trouvée dans la section
\texttt{Data\ Model} de
\href{https://docs.python.org/3/reference/datamodel.html}{la
documentation du langage Python}.
All operators are also exposed as ordinary functions in the operators
module. The documentation of that module gives a good overview of Python
operators. It can be found at
\url{https://docs.python.org/3.9/library/operator.html}
If we say that an object implements a specific language protocol, it
means that it is compatible with a specific part of the Python language
syntax.
The following is a table of the most common protocols within the Python
language.
Protocol nameMethodsDescriptionCallable protocol\emph{call}()Allows
objects to be called with parentheses:instance()Descriptor
protocols\emph{set}(), \emph{get}(), and \emph{del}()Allows us to
manipulate the attribute access pattern of classes (see the Descriptors
section)Container protocol\emph{contains}()Allows us to test whether or
not an object contains some value using the in keyword:value in instance
Python in Comparison with Other LanguagesIterable
protocol\emph{iter}()Allows objects to be iterated using the
forkeyword:for value in instance: \ldots\hspace{0pt}Sequence
protocol\emph{getitem}(),\emph{len}()Allows objects to be indexed with
square bracket syntax and queried for length using a built-in
function:item = instance{[}index{]}length = len(instance)Each operator
available in Python has its own protocol and operator overloading
happens by implementing the dunder methods of that protocol. Python
provides over 50 overloadable operators that can be divided into five
main groups:• Arithmetic operators • In-place assignment operators•
Comparison operators• Identity operators• Bitwise operatorsThat's a lot
of protocols so we won't discuss all of them here. We will instead take
a look at a practical example that will allow you to better understand
how to implement operator overloading on your own
The \texttt{add()} method is responsible for overloading the \texttt{+}
(plus sign) operator and here it allows us to add two matrices together.
Only matrices of the same dimensions can be added together. This is a
fairly simple operation that involves adding all matrix elements one by
one to form a new matrix.
The \texttt{sub()} method is responsible for overloading the \texttt{}
(minus sign) operator that will be responsible for matrix subtraction.
To subtract two matrices, we use a similar technique as in the --
operator:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def} \FunctionTok{\_\_sub\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, other):}
\ControlFlowTok{if}\NormalTok{ (}\BuiltInTok{len}\NormalTok{(}\VariableTok{self}\NormalTok{.rows) }\OperatorTok{!=} \BuiltInTok{len}\NormalTok{(other.rows) }\KeywordTok{or} \BuiltInTok{len}\NormalTok{(}\VariableTok{self}\NormalTok{.rows[}\DecValTok{0}\NormalTok{]) }\OperatorTok{!=} \BuiltInTok{len}\NormalTok{(other.rows[}\DecValTok{0}\NormalTok{])):}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Matrix dimensions don\textquotesingle{}t match"}\NormalTok{)}
\ControlFlowTok{return}\NormalTok{ Matrix([[a }\OperatorTok{{-}}\NormalTok{ b }\ControlFlowTok{for}\NormalTok{ a, b }\KeywordTok{in} \BuiltInTok{zip}\NormalTok{(a\_row, b\_row)] }\ControlFlowTok{for}\NormalTok{ a\_row, b\_row }\KeywordTok{in} \BuiltInTok{zip}\NormalTok{(}\VariableTok{self}\NormalTok{.rows, other.rows) ])}
\end{Highlighting}
\end{Shaded}
And the following is the last method we add to our class:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def} \FunctionTok{\_\_mul\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, other):}
\ControlFlowTok{if} \KeywordTok{not} \BuiltInTok{isinstance}\NormalTok{(other, Matrix):}
\ControlFlowTok{raise} \PreprocessorTok{TypeError}\NormalTok{(}\SpecialStringTok{f"Don\textquotesingle{}t know how to multiply }\SpecialCharTok{\{}\BuiltInTok{type}\NormalTok{(other)}\SpecialCharTok{\}}\SpecialStringTok{ with Matrix"}\NormalTok{)}
\ControlFlowTok{if} \BuiltInTok{len}\NormalTok{(}\VariableTok{self}\NormalTok{.rows[}\DecValTok{0}\NormalTok{]) }\OperatorTok{!=} \BuiltInTok{len}\NormalTok{(other.rows):}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Matrix dimensions don\textquotesingle{}t match"}\NormalTok{)}
\NormalTok{ rows }\OperatorTok{=}\NormalTok{ [[}\DecValTok{0} \ControlFlowTok{for}\NormalTok{ \_ }\KeywordTok{in}\NormalTok{ other.rows[}\DecValTok{0}\NormalTok{]] }\ControlFlowTok{for}\NormalTok{ \_ }\KeywordTok{in} \VariableTok{self}\NormalTok{.rows]}
\ControlFlowTok{for}\NormalTok{ i }\KeywordTok{in} \BuiltInTok{range}\NormalTok{(}\BuiltInTok{len}\NormalTok{ (}\VariableTok{self}\NormalTok{.rows)):}
\ControlFlowTok{for}\NormalTok{ j }\KeywordTok{in} \BuiltInTok{range}\NormalTok{(}\BuiltInTok{len}\NormalTok{ (other.rows[}\DecValTok{0}\NormalTok{])):}
\ControlFlowTok{for}\NormalTok{ k }\KeywordTok{in} \BuiltInTok{range}\NormalTok{(}\BuiltInTok{len}\NormalTok{ (other.rows)):}
\NormalTok{ rows[i][j] }\OperatorTok{+=} \VariableTok{self}\NormalTok{.rows[i][k] }\OperatorTok{*}\NormalTok{ other.rows[k][j]}
\ControlFlowTok{return}\NormalTok{ Matrix(rows)}
\end{Highlighting}
\end{Shaded}
The last overloaded operator is the most complex one. This is the
\texttt{*} operator, which is implemented through the \texttt{mul()}
method. In linear algebra, matrices don't have the same multiplication
operation as real numbers. Two matrices can be multiplied if the first
matrix has a number of columns equal to the number of rows of the second
matrix. The result of that operation is a new matrix where each element
is a dot product of the corresponding row of the first matrix and the
corresponding column of the second matrix. Here we've built our own
implementation of the matrix to present the idea of operators
overloading. Although Python lacks a built-in type for matrices, you
don't need to build them from scratch. The NumPy package is one of the
best Python mathematical packages and among others provides native
support for matrix algebra. You can easily obtain the NumPy package from
PyPI
En fait, l'intérêt concerne surtout la représentation de nos modèles,
puisque chaque classe du modèle est représentée par la définition d'un
objet Python. Nous pouvons donc utiliser ces mêmes \textbf{dunder
methods} (\textbf{double-underscores methods}) pour étoffer les
protocoles du langage.
\hypertarget{_the_zen_of_python}{%
\subsection{The Zen of Python}\label{_the_zen_of_python}}
\begin{Shaded}
\begin{Highlighting}[]
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}} \ImportTok{import}\NormalTok{ this}
\end{Highlighting}
\end{Shaded}
\begin{verbatim}
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
\end{verbatim}
\hypertarget{_pep8_style_guide_for_python_code}{%
\subsection{PEP8 - Style Guide for Python
Code}\label{_pep8_style_guide_for_python_code}}
La première PEP qui va nous intéresser est la
\href{https://www.python.org/dev/peps/pep-0008/}{PEP 8---Style Guide
for Python Code}. Elle spécifie comment du code Python doit être
organisé ou formaté, quelles sont les conventions pour l'indentation, le
nommage des variables et des classes, \ldots\hspace{0pt} En bref, elle
décrit comment écrire du code proprement, afin que d'autres développeurs
puissent le reprendre facilement, ou simplement que votre base de code
ne dérive lentement vers un seuil de non-maintenabilité.
Dans cet objectif, un outil existe et listera l'ensemble des conventions
qui ne sont pas correctement suivies dans votre projet: pep8. Pour
l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du
chemin à analyser (\texttt{.}, le nom d'un répertoire, le nom d'un
fichier \texttt{.py}, \ldots\hspace{0pt}). Si vous souhaitez uniquement
avoir le nombre d'erreur de chaque type, saisissez les options
\texttt{-\/-statistics\ -qq}.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{pep8}\NormalTok{ . {-}{-}statistics {-}qq}
\ExtensionTok{7}\NormalTok{ E101 indentation contains mixed spaces and tabs}
\ExtensionTok{6}\NormalTok{ E122 continuation line missing indentation or outdented}
\ExtensionTok{8}\NormalTok{ E127 continuation line over{-}indented for visual indent}
\ExtensionTok{23}\NormalTok{ E128 continuation line under{-}indented for visual indent}
\ExtensionTok{3}\NormalTok{ E131 continuation line unaligned for hanging indent}
\ExtensionTok{12}\NormalTok{ E201 whitespace after }\StringTok{\textquotesingle{}\{\textquotesingle{}}
\ExtensionTok{13}\NormalTok{ E202 whitespace before }\StringTok{\textquotesingle{}\}\textquotesingle{}}
\ExtensionTok{86}\NormalTok{ E203 whitespace before }\StringTok{\textquotesingle{}:\textquotesingle{}}
\end{Highlighting}
\end{Shaded}
Si vous ne voulez pas être dérangé sur votre manière de coder, et que
vous voulez juste avoir un retour sur une analyse de votre code, essayez
\texttt{pyflakes}: cette librairie analysera vos sources à la recherche
de sources d'erreurs possibles (imports inutilisés, méthodes inconnues,
etc.).
\hypertarget{_pep257_docstring_conventions}{%
\subsection{PEP257 - Docstring
Conventions}\label{_pep257_docstring_conventions}}
Python étant un langage interprété fortement typé, il est plus que
conseillé, au même titre que les tests unitaires que nous verrons plus
bas, de documenter son code. Cela impose une certaine rigueur, mais
améliore énormément la qualité, la compréhension et la reprise du code
par une tierce personne. Cela implique aussi de \textbf{tout}
documenter: les modules, les paquets, les classes, les fonctions,
méthodes, \ldots\hspace{0pt} Ce qui peut également aller à contrecourant
d'autres pratiques cite:{[}clean\_code(53-74){]}; il y a une juste
mesure à prendre entre "tout documenter" et "tout bien documenter":
\begin{itemize}
\item
Inutile d'ajouter des watermarks, auteurs, \ldots\hspace{0pt} Git ou
tout VCS s'en sortira très bien et sera beaucoup plus efficace que
n'importe quelle chaîne de caractères que vous pourriez indiquer et
qui sera fausse dans six mois,
\item
Inutile de décrire quelque chose qui est évident; documenter la
méthode \texttt{get\_age()} d'une personne n'aura pas beaucoup
d'intérêt
\item
S'il est nécessaire de décrire un comportement au sein-même d'une
fonction, c'est que ce comportement pourrait être extrait dans une
nouvelle fonction (qui, elle, pourra être documentée)
\end{itemize}
Documentation: be obsessed! Mais \textbf{le code reste la référence}
Il existe plusieurs types de conventions de documentation:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
PEP 257
\item
Numpy
\item
Google Style (parfois connue sous l'intitulé \texttt{Napoleon})
\item
\ldots\hspace{0pt}
\end{enumerate}
Les
\href{https://google.github.io/styleguide/pyguide.html\#38-comments-and-docstrings}{conventions
proposées par Google} nous semblent plus faciles à lire que du
RestructuredText, mais sont parfois moins bien intégrées que les
docstrings officiellement supportées (par exemple,
\href{https://clize.readthedocs.io/en/stable/}{clize} ne reconnait que
du RestructuredText;
\href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{l'auto-documentation}
de Django également). L'exemple donné dans les guides de style de Google
est celui-ci:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ fetch\_smalltable\_rows(table\_handle: smalltable.Table,}
\NormalTok{ keys: Sequence[Union[}\BuiltInTok{bytes}\NormalTok{, }\BuiltInTok{str}\NormalTok{]],}
\NormalTok{ require\_all\_keys: }\BuiltInTok{bool} \OperatorTok{=} \VariableTok{False}\NormalTok{,}
\NormalTok{) }\OperatorTok{{-}\textgreater{}}\NormalTok{ Mapping[}\BuiltInTok{bytes}\NormalTok{, Tuple[}\BuiltInTok{str}\NormalTok{]]:}
\CommentTok{"""Fetches rows from a Smalltable.}
\CommentTok{ Retrieves rows pertaining to the given keys from the Table instance}
\CommentTok{ represented by table\_handle. String keys will be UTF{-}8 encoded.}
\CommentTok{ Args:}
\CommentTok{ table\_handle: An open smalltable.Table instance.}
\CommentTok{ keys: A sequence of strings representing the key of each table}
\CommentTok{ row to fetch. String keys will be UTF{-}8 encoded.}
\CommentTok{ require\_all\_keys: Optional; If require\_all\_keys is True only}
\CommentTok{ rows with values set for all keys will be returned.}
\CommentTok{ Returns:}
\CommentTok{ A dict mapping keys to the corresponding table row data}
\CommentTok{ fetched. Each row is represented as a tuple of strings. For}
\CommentTok{ example:}
\CommentTok{ \{b\textquotesingle{}Serak\textquotesingle{}: (\textquotesingle{}Rigel VII\textquotesingle{}, \textquotesingle{}Preparer\textquotesingle{}),}
\CommentTok{ b\textquotesingle{}Zim\textquotesingle{}: (\textquotesingle{}Irk\textquotesingle{}, \textquotesingle{}Invader\textquotesingle{}),}
\CommentTok{ b\textquotesingle{}Lrrr\textquotesingle{}: (\textquotesingle{}Omicron Persei 8\textquotesingle{}, \textquotesingle{}Emperor\textquotesingle{})\}}
\CommentTok{ Returned keys are always bytes. If a key from the keys argument is}
\CommentTok{ missing from the dictionary, then that row was not found in the}
\CommentTok{ table (and require\_all\_keys must have been False).}
\CommentTok{ Raises:}
\CommentTok{ IOError: An error occurred accessing the smalltable.}
\CommentTok{ """}
\end{Highlighting}
\end{Shaded}
C'est-à-dire:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Une courte ligne d'introduction, descriptive, indiquant ce que la
fonction ou la méthode réalise. Attention, la documentation ne doit
pas indiquer \emph{comment} la fonction/méthode est implémentée, mais
ce qu'elle fait concrètement (et succintement).
\item
Une ligne vide
\item
Une description plus complète et plus verbeuse, si vous le jugez
nécessaire
\item
Une ligne vide
\item
La description des arguments et paramètres, des valeurs de retour, des
exemples et les exceptions qui peuvent être levées.
\end{enumerate}
Un exemple (encore) plus complet peut être trouvé
\href{https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html\#example-google}{dans
le dépôt sphinxcontrib-napoleon}. Et ici, nous tombons peut-être dans
l'excès de zèle:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ module\_level\_function(param1, param2}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{*}\NormalTok{args, }\OperatorTok{**}\NormalTok{kwargs):}
\CommentTok{"""This is an example of a module level function.}
\CommentTok{ Function parameters should be documented in the \textasciigrave{}\textasciigrave{}Args\textasciigrave{}\textasciigrave{} section. The name}
\CommentTok{ of each parameter is required. The type and description of each parameter}
\CommentTok{ is optional, but should be included if not obvious.}
\CommentTok{ If \textbackslash{}*args or \textbackslash{}*\textbackslash{}*kwargs are accepted,}
\CommentTok{ they should be listed as \textasciigrave{}\textasciigrave{}*args\textasciigrave{}\textasciigrave{} and \textasciigrave{}\textasciigrave{}**kwargs\textasciigrave{}\textasciigrave{}.}
\CommentTok{ The format for a parameter is::}
\CommentTok{ name (type): description}
\CommentTok{ The description may span multiple lines. Following}
\CommentTok{ lines should be indented. The "(type)" is optional.}
\CommentTok{ Multiple paragraphs are supported in parameter}
\CommentTok{ descriptions.}
\CommentTok{ Args:}
\CommentTok{ param1 (int): The first parameter.}
\CommentTok{ param2 (:obj:\textasciigrave{}str\textasciigrave{}, optional): The second parameter. Defaults to None.}
\CommentTok{ Second line of description should be indented.}
\CommentTok{ *args: Variable length argument list.}
\CommentTok{ **kwargs: Arbitrary keyword arguments.}
\CommentTok{ Returns:}
\CommentTok{ bool: True if successful, False otherwise.}
\CommentTok{ The return type is optional and may be specified at the beginning of}
\CommentTok{ the \textasciigrave{}\textasciigrave{}Returns\textasciigrave{}\textasciigrave{} section followed by a colon.}
\CommentTok{ The \textasciigrave{}\textasciigrave{}Returns\textasciigrave{}\textasciigrave{} section may span multiple lines and paragraphs.}
\CommentTok{ Following lines should be indented to match the first line.}
\CommentTok{ The \textasciigrave{}\textasciigrave{}Returns\textasciigrave{}\textasciigrave{} section supports any reStructuredText formatting,}
\CommentTok{ including literal blocks::}
\CommentTok{ \{}
\CommentTok{ \textquotesingle{}param1\textquotesingle{}: param1,}
\CommentTok{ \textquotesingle{}param2\textquotesingle{}: param2}
\CommentTok{ \}}
\CommentTok{ Raises:}
\CommentTok{ AttributeError: The \textasciigrave{}\textasciigrave{}Raises\textasciigrave{}\textasciigrave{} section is a list of all exceptions}
\CommentTok{ that are relevant to the interface.}
\CommentTok{ ValueError: If \textasciigrave{}param2\textasciigrave{} is equal to \textasciigrave{}param1\textasciigrave{}.}
\CommentTok{ """}
\ControlFlowTok{if}\NormalTok{ param1 }\OperatorTok{==}\NormalTok{ param2:}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{\textquotesingle{}param1 may not be equal to param2\textquotesingle{}}\NormalTok{)}
\ControlFlowTok{return} \VariableTok{True}
\end{Highlighting}
\end{Shaded}
Pour ceux que cela pourrait intéresser, il existe
\href{https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring}{une
extension pour Codium}, comme nous le verrons juste après, qui permet de
générer automatiquement le squelette de documentation d'un bloc de code:
\begin{figure}
\centering
\includegraphics{images/environment/python-docstring-vscode.png}
\caption{autodocstring}
\end{figure}
Nous le verrons plus loin, Django permet de rendre la documentation
immédiatement accessible depuis son interface d'administration. Toute
information pertinente peut donc lier le code à un cas d'utilisation
concret.
\hypertarget{_linters}{%
\subsection{Linters}\label{_linters}}
Il existe plusieurs niveaux de \emph{linters}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le premier niveau concerne
\href{https://pypi.org/project/pycodestyle/}{pycodestyle}
(anciennement, \texttt{pep8} justement\ldots\hspace{0pt}), qui analyse
votre code à la recherche d'erreurs de convention.
\item
Le deuxième niveau concerne
\href{https://pypi.org/project/pyflakes/}{pyflakes}. Pyflakes est un
\emph{simple} \footnote{Ce n'est pas moi qui le dit, c'est la doc du
projet} programme qui recherchera des erreurs parmi vos fichiers
Python.
\item
Le troisième niveau est
\href{https://pypi.org/project/flake8/}{Flake8}, qui regroupe les deux
premiers niveaux, en plus d'y ajouter flexibilité, extensions et une
analyse de complexité de McCabe.
\item
Le quatrième niveau \footnote{Oui, en Python, il n'y a que quatre
cercles à l'Enfer} est \href{https://pylint.org/}{PyLint}.
\end{enumerate}
PyLint est le meilleur ami de votre \emph{moi} futur, un peu comme quand
vous prenez le temps de faire la vaisselle pour ne pas avoir à la faire
le lendemain: il rendra votre code soyeux et brillant, en posant des
affirmations spécifiques. A vous de les traiter en corrigeant le code ou
en apposant un \emph{tag} indiquant que vous avez pris connaissance de
la remarque, que vous en avez tenu compte, et que vous choisissez malgré
tout de faire autrement.
Pour vous donner une idée, voici ce que cela pourrait donner avec un
code pas très propre et qui ne sert à rien:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ datetime}
\CommentTok{"""On stocke la date du jour dans la variable ToD4y"""}
\NormalTok{ToD4y }\OperatorTok{=}\NormalTok{ datetime.today()}
\KeywordTok{def}\NormalTok{ print\_today(ToD4y):}
\NormalTok{ today }\OperatorTok{=}\NormalTok{ ToD4y}
\BuiltInTok{print}\NormalTok{(ToD4y)}
\KeywordTok{def}\NormalTok{ GetToday():}
\ControlFlowTok{return}\NormalTok{ ToD4y}
\ControlFlowTok{if} \VariableTok{\_\_name\_\_} \OperatorTok{==} \StringTok{"\_\_main\_\_"}\NormalTok{:}
\NormalTok{ t }\OperatorTok{=}\NormalTok{ Get\_Today()}
\BuiltInTok{print}\NormalTok{(t)}
\end{Highlighting}
\end{Shaded}
Avec Flake8, nous obtiendrons ceci:
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{test.py}\NormalTok{:7:1: E302 expected 2 blank lines, found 1}
\ExtensionTok{test.py}\NormalTok{:8:5: F841 local variable }\StringTok{\textquotesingle{}today\textquotesingle{}}\NormalTok{ is assigned to but never used}
\ExtensionTok{test.py}\NormalTok{:11:1: E302 expected 2 blank lines, found 1}
\ExtensionTok{test.py}\NormalTok{:16:8: E222 multiple spaces after operator}
\ExtensionTok{test.py}\NormalTok{:16:11: F821 undefined name }\StringTok{\textquotesingle{}Get\_Today\textquotesingle{}}
\ExtensionTok{test.py}\NormalTok{:18:1: W391 blank line at end of file}
\end{Highlighting}
\end{Shaded}
Nous trouvons des erreurs:
\begin{itemize}
\item
de \textbf{conventions}: le nombre de lignes qui séparent deux
fonctions, le nombre d'espace après un opérateur, une ligne vide à la
fin du fichier, \ldots\hspace{0pt} Ces \emph{erreurs} n'en sont pas
vraiment, elles indiquent juste de potentiels problèmes de
communication si le code devait être lu ou compris par une autre
personne.
\item
de \textbf{définition}: une variable assignée mais pas utilisée ou une
lexème non trouvé. Cette dernière information indique clairement un
bug potentiel. Ne pas en tenir compte nuira sans doute à la santé de
votre code (et risque de vous réveiller à cinq heures du mat', quand
votre application se prendra méchamment les pieds dans le tapis).
\end{itemize}
L'étape d'après consiste à invoquer pylint. Lui, il est directement
moins conciliant:
\begin{verbatim}
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10
\end{verbatim}
En gros, j'ai programmé comme une grosse bouse anémique (et oui: le
score d'évaluation du code permet bien d'aller en négatif). En vrac,
nous trouvons des problèmes liés:
\begin{itemize}
\item
au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)
\item
à des variables non définies (E0602)
\item
de la documentation manquante (C0114, C0116)
\item
de la redéfinition de variables (W0621).
\end{itemize}
Pour reprendre la
\href{http://pylint.pycqa.org/en/latest/user_guide/message-control.html}{documentation},
chaque code possède sa signification (ouf!):
\begin{itemize}
\item
C convention related checks
\item
R refactoring related checks
\item
W various warnings
\item
E errors, for probable bugs in the code
\item
F fatal, if an error occurred which prevented pylint from doing
further* processing.
\end{itemize}
TODO: Expliquer comment faire pour tagger une explication.
TODO: Voir si la sortie de pylint est obligatoirement 0 s'il y a un
warning
TODO: parler de \texttt{pylint\ -\/-errors-only}
\hypertarget{_formatage_de_code}{%
\subsection{Formatage de code}\label{_formatage_de_code}}
Nous avons parlé ci-dessous de style de codage pour Python (PEP8), de
style de rédaction pour la documentation (PEP257), d'un \emph{linter}
pour nous indiquer quels morceaux de code doivent absolument être revus,
\ldots\hspace{0pt} Reste que ces tâches sont parfois (très) souvent
fastidieuses: écrire un code propre et systématiquement cohérent est une
tâche ardue. Heureusement, il existe des outils pour nous aider (un
peu).
A nouveau, il existe plusieurs possibilités de formatage automatique du
code. Même si elle n'est pas parfaite,
\href{https://black.readthedocs.io/en/stable/}{Black} arrive à un
compromis entre clarté du code, facilité d'installation et d'intégration
et résultat.
Est-ce que ce formatage est idéal et accepté par tout le monde ?
\textbf{Non}. Même Pylint arrivera parfois à râler. Mais ce formatage
conviendra dans 97,83\% des cas (au moins).
\begin{quote}
By using Black, you agree to cede control over minutiae of
hand-formatting. In return, Black gives you speed, determinism, and
freedom from pycodestyle nagging about formatting. You will save time
and mental energy for more important matters.
Black makes code review faster by producing the smallest diffs possible.
Blackened code looks the same regardless of the project you're reading.
Formatting becomes transparent after a while and you can focus on the
content instead.
\end{quote}
Traduit rapidement à partir de la langue de Batman: "\emph{En utilisant
Black, vous cédez le contrôle sur le formatage de votre code. En retour,
Black vous fera gagner un max de temps, diminuera votre charge mentale
et fera revenir l'être aimé}". Mais la partie réellement intéressante
concerne le fait que "\emph{Tout code qui sera passé par Black aura la
même forme, indépendamment du project sur lequel vous serez en train de
travailler. L'étape de formatage deviendra transparente, et vous pourrez
vous concentrer sur le contenu}".
\hypertarget{_complexituxe9_cyclomatique}{%
\subsection{Complexité cyclomatique}\label{_complexituxe9_cyclomatique}}
A nouveau, un greffon pour \texttt{flake8} existe et donnera une
estimation de la complexité de McCabe pour les fonctions trop complexes.
Installez-le avec \texttt{pip\ install\ mccabe}, et activez-le avec le
paramètre \texttt{-\/-max-complexity}. Toute fonction dans la complexité
est supérieure à cette valeur sera considérée comme trop complexe.
\hypertarget{_typage_statique_pep585}{%
\subsection{\texorpdfstring{Typage statique -
\href{https://www.python.org/dev/peps/pep-0585/}{PEP585}}{Typage statique - PEP585}}\label{_typage_statique_pep585}}
Nous vous disions ci-dessus que Python était un langage dynamique
interprété. Concrètement, cela signifie que des erreurs pouvant être
détectées à la compilation avec d'autres langages, ne le sont pas avec
Python.
Il existe cependant une solution à ce problème, sous la forme de
\href{http://mypy-lang.org/}{Mypy}, qui peut (sous vous le souhaitez
;-)) vérifier une forme de typage statique de votre code source, grâce à
une expressivité du code, basée sur des annotations (facultatives, elles
aussi).
Ces vérifications se présentent de la manière suivante:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ typing }\ImportTok{import}\NormalTok{ List}
\KeywordTok{def}\NormalTok{ first\_int\_elem(l: List[}\BuiltInTok{int}\NormalTok{]) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return}\NormalTok{ l[}\DecValTok{0}\NormalTok{] }\ControlFlowTok{if}\NormalTok{ l }\ControlFlowTok{else} \VariableTok{None}
\ControlFlowTok{if} \VariableTok{\_\_name\_\_} \OperatorTok{==} \StringTok{"\_\_main\_\_"}\NormalTok{:}
\BuiltInTok{print}\NormalTok{(first\_int\_elem([}\DecValTok{1}\NormalTok{, }\DecValTok{2}\NormalTok{, }\DecValTok{3}\NormalTok{]))}
\BuiltInTok{print}\NormalTok{(first\_int\_elem([}\StringTok{\textquotesingle{}a\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}b\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}c\textquotesingle{}}\NormalTok{]))}
\end{Highlighting}
\end{Shaded}
Est-ce que le code ci-dessous fonctionne correctement ? \textbf{Oui}:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{python}\NormalTok{ mypy{-}test.py}
\ExtensionTok{1}
\ExtensionTok{a}
\end{Highlighting}
\end{Shaded}
Malgré que nos annotations déclarent une liste d'entiers, rien ne nous
empêche de lui envoyer une liste de caractères, sans que cela ne lui
pose de problèmes.
Est-ce que Mypy va râler ? \textbf{Oui, aussi}. Non seulement nous
retournons la valeur \texttt{None} si la liste est vide alors que nous
lui annoncions un entier en sortie, mais en plus, nous l'appelons avec
une liste de caractères, alors que nous nous attendions à une liste
d'entiers:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{mypy}\NormalTok{ mypy{-}test.py}
\ExtensionTok{mypy{-}test.py}\NormalTok{:7: error: Incompatible return value type (got }\StringTok{"Optional[int]"}\NormalTok{, expected }\StringTok{"int"}\NormalTok{)}
\ExtensionTok{mypy{-}test.py}\NormalTok{:12: error: List item 0 has incompatible type }\StringTok{"str"}\KeywordTok{;} \ExtensionTok{expected} \StringTok{"int"}
\ExtensionTok{mypy{-}test.py}\NormalTok{:12: error: List item 1 has incompatible type }\StringTok{"str"}\KeywordTok{;} \ExtensionTok{expected} \StringTok{"int"}
\ExtensionTok{mypy{-}test.py}\NormalTok{:12: error: List item 2 has incompatible type }\StringTok{"str"}\KeywordTok{;} \ExtensionTok{expected} \StringTok{"int"}
\ExtensionTok{Found}\NormalTok{ 4 errors in 1 file (checked 1 source file)}
\end{Highlighting}
\end{Shaded}
Pour corriger ceci, nous devons:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Importer le type \texttt{Optional} et l'utiliser en sortie de notre
fonction \texttt{first\_int\_elem}
\item
Eviter de lui donner de mauvais paramètres ;-)
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ typing }\ImportTok{import}\NormalTok{ List, Optional}
\KeywordTok{def}\NormalTok{ first\_int\_elem(l: List[}\BuiltInTok{int}\NormalTok{]) }\OperatorTok{{-}\textgreater{}}\NormalTok{ Optional[}\BuiltInTok{int}\NormalTok{]:}
\ControlFlowTok{return}\NormalTok{ l[}\DecValTok{0}\NormalTok{] }\ControlFlowTok{if}\NormalTok{ l }\ControlFlowTok{else} \VariableTok{None}
\ControlFlowTok{if} \VariableTok{\_\_name\_\_} \OperatorTok{==} \StringTok{"\_\_main\_\_"}\NormalTok{:}
\BuiltInTok{print}\NormalTok{(first\_int\_elem([}\DecValTok{1}\NormalTok{, }\DecValTok{2}\NormalTok{, }\DecValTok{3}\NormalTok{]))}
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{mypy}\NormalTok{ mypy{-}test.py}
\ExtensionTok{Success}\NormalTok{: no issues found in 1 source file}
\end{Highlighting}
\end{Shaded}
\hypertarget{_tests_unitaires}{%
\subsubsection{Tests unitaires}\label{_tests_unitaires}}
\textbf{→ PyTest}
Comme tout bon \textbf{framework} qui se respecte, Django embarque tout
un environnement facilitant le lancement de tests; 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}:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
\KeywordTok{class}\NormalTok{ TestModel(TestCase):}
\KeywordTok{def}\NormalTok{ test\_str(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{raise} \PreprocessorTok{NotImplementedError}\NormalTok{(}\StringTok{\textquotesingle{}Not implemented yet\textquotesingle{}}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
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, \ldots\hspace{0pt}); on ne parlera ici
que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux.
C'est là qu'il est utile d'avoir le pourcentage de code couvert par les
différents tests, pour savoir ce qui peut être amélioré.
Comme indiqué ci-dessus, Django propose son propre cadre de tests, au
travers du package \texttt{django.tests}. Une bonne pratique (parfois
discutée) consiste cependant à switcher vers \texttt{pytest}, qui
présente quelques avantages:
\begin{itemize}
\item
Une syntaxe plus concise (au prix de
\href{https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html}{quelques
conventions}, même si elles restent configurables): un test est une
fonction, et ne doit pas obligatoirement faire partie d'une classe
héritant de \texttt{TestCase} - la seule nécessité étant que cette
fonction fasse partie d'un module commençant ou finissant par "test"
(\texttt{test\_example.py} ou \texttt{example\_test.py}).
\item
Une compatibilité avec du code Python "classique" - vous ne devrez
donc retenir qu'un seul ensemble de commandes ;-)
\item
Des \emph{fixtures} faciles à réutiliser entre vos différents
composants
\item
Une compatibilité avec le reste de l'écosystème, dont la couverture de
code présentée ci-dessous.
\end{itemize}
Ainsi, après installation, il nous suffit de créer notre module
\texttt{test\_models.py}, dans lequel nous allons simplement tester
l'addition d'un nombre et d'une chaîne de caractères (oui, c'est
complètement biesse; on est sur la partie théorique ici):
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ test\_add():}
\ControlFlowTok{assert} \DecValTok{1} \OperatorTok{+} \DecValTok{1} \OperatorTok{==} \StringTok{"argh"}
\end{Highlighting}
\end{Shaded}
Forcément, cela va planter. Pour nous en assurer (dès fois que quelqu'un
en doute), il nous suffit de démarrer la commande \texttt{pytest}:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{pytest}
\NormalTok{============================= }\BuiltInTok{test}\NormalTok{ session starts ====================================}
\ExtensionTok{platform}\NormalTok{ ...}
\ExtensionTok{rootdir}\NormalTok{: ...}
\ExtensionTok{plugins}\NormalTok{: django{-}4.1.0}
\ExtensionTok{collected}\NormalTok{ 1 item}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}test\_models.py F [100\%]}
\NormalTok{================================== }\ExtensionTok{FAILURES}\NormalTok{ ==========================================}
\ExtensionTok{\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_}\NormalTok{ test\_basic\_add \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_}
\ExtensionTok{def}\NormalTok{ test\_basic\_add()}\BuiltInTok{:}
\OperatorTok{\textgreater{}} \ExtensionTok{assert}\NormalTok{ 1 + 1 == }\StringTok{"argh"}
\ExtensionTok{E}\NormalTok{ AssertionError: assert (1 + 1) == }\StringTok{\textquotesingle{}argh\textquotesingle{}}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}test\_models.py:2: AssertionError}
\NormalTok{=========================== }\ExtensionTok{short}\NormalTok{ test summary info ==================================}
\ExtensionTok{FAILED}\NormalTok{ gwift/test\_models.py::test\_basic\_add {-} AssertionError: assert (1 + 1) == }\StringTok{\textquotesingle{}argh\textquotesingle{}}
\NormalTok{============================== }\ExtensionTok{1}\NormalTok{ failed in 0.10s =====================================}
\end{Highlighting}
\end{Shaded}
\hypertarget{_couverture_de_code}{%
\subsubsection{Couverture de code}\label{_couverture_de_code}}
La couverture de code est une analyse qui donne un pourcentage lié à la
quantité de code couvert par les tests. Attention qu'il ne s'agit pas de
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.
Avec \texttt{pytest}, il convient d'utiliser le paquet
\href{https://pypi.org/project/pytest-cov/}{\texttt{pytest-cov}}, suivi
de la commande \texttt{pytest\ -\/-cov=gwift\ tests/}.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez
passer par le paquet
\href{https://pypi.org/project/django-coverage-plugin/}{django-coverage-plugin}
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.
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# requirements/base.text}
\NormalTok{[}\ExtensionTok{...}\NormalTok{]}
\ExtensionTok{django\_coverage\_plugin}
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# .coveragerc to control coverage.py}
\NormalTok{[}\ExtensionTok{run}\NormalTok{]}
\ExtensionTok{branch}\NormalTok{ = True}
\ExtensionTok{omit}\NormalTok{ = ../*migrations*}
\ExtensionTok{plugins}\NormalTok{ =}
\ExtensionTok{django\_coverage\_plugin}
\NormalTok{[}\ExtensionTok{report}\NormalTok{]}
\ExtensionTok{ignore\_errors}\NormalTok{ = True}
\NormalTok{[}\ExtensionTok{html}\NormalTok{]}
\ExtensionTok{directory}\NormalTok{ = coverage\_html\_report}
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{coverage}\NormalTok{ run {-}{-}source }\StringTok{"."}\NormalTok{ manage.py test}
\NormalTok{$ }\ExtensionTok{coverage}\NormalTok{ report}
\ExtensionTok{Name}\NormalTok{ Stmts Miss Cover}
\ExtensionTok{{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}\_\_init\_\_.py 0 0 100\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}settings.py 17 0 100\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}urls.py 5 5 0\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}wsgi.py 4 4 0\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}manage.py 6 0 100\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}\_\_init\_\_.py 0 0 100\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}admin.py 1 0 100\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}models.py 49 16 67\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}tests.py 1 1 0\%}
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}views.py 6 6 0\%}
\ExtensionTok{{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}}
\ExtensionTok{TOTAL}\NormalTok{ 89 32 64\%}
\ExtensionTok{{-}{-}{-}{-}}
\NormalTok{$ }\ExtensionTok{coverage}\NormalTok{ html}
\end{Highlighting}
\end{Shaded}
-\/- / partie obsolète -\/-
Ceci vous affichera non seulement la couverture de code estimée, et
générera également vos fichiers sources avec les branches non couvertes.
\hypertarget{_matrice_de_compatibilituxe9}{%
\subsubsection{Matrice de
compatibilité}\label{_matrice_de_compatibilituxe9}}
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. Nous
sommes donc un cran plus haut que la spécification des versions des
librairies, puisque nous nous situons directement au niveau de
l'interpréteur.
L'outil le plus connu est
\href{https://tox.readthedocs.io/en/latest/}{Tox}, qui consiste en un
outil basé sur virtualenv et qui permet:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
de vérifier que votre application s'installe correctement avec
différentes versions de Python et d'interpréteurs
\item
de démarrer des tests parmi ces différents environnements
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# content of: tox.ini , put in same dir as setup.py}
\KeywordTok{[tox]}
\DataTypeTok{envlist }\OtherTok{=}\StringTok{ py36,py37,py38,py39}
\DataTypeTok{skipsdist }\OtherTok{=}\StringTok{ }\KeywordTok{true}
\KeywordTok{[testenv]}
\DataTypeTok{deps }\OtherTok{=}
\DataTypeTok{ {-}r requirements/dev.txt}
\DataTypeTok{commands }\OtherTok{=}
\DataTypeTok{ pytest}
\end{Highlighting}
\end{Shaded}
Démarrez ensuite la commande \texttt{tox}, pour démarrer la commande
\texttt{pytest} sur les environnements Python 3.6, 3.7, 3.8 et 3.9,
après avoir installé nos dépendances présentes dans le fichier
\texttt{requirements/dev.txt}.
pour que la commande ci-dessus fonctionne correctement, il sera
nécessaire que vous ayez les différentes versions d'interpréteurs
installées. Ci-dessus, la commande retournera une erreur pour chaque
version non trouvée, avec une erreur type
\texttt{ERROR:\ \ \ pyXX:\ InterpreterNotFound:\ pythonX.X}.
\hypertarget{_configuration_globale}{%
\subsubsection{Configuration globale}\label{_configuration_globale}}
Décrire le fichier setup.cfg
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\FunctionTok{touch}\NormalTok{ setup.cfg}
\end{Highlighting}
\end{Shaded}
\hypertarget{_dockerfile}{%
\subsubsection{Dockerfile}\label{_dockerfile}}
\begin{verbatim}
# Dockerfile
# Pull base image
#FROM python:3.8
FROM python:3.8-slim-buster
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV DEBIAN_FRONTEND noninteractive
ENV ACCEPT_EULA=Y
# install Microsoft SQL Server requirements.
ENV ACCEPT_EULA=Y
RUN apt-get update -y && apt-get update \
&& apt-get install -y --no-install-recommends curl gcc g++ gnupg
# Add SQL Server ODBC Driver 17
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get update \
&& apt-get install -y msodbcsql17 unixodbc-dev
# clean the install.
RUN apt-get -y clean
# Set work directory
WORKDIR /code
# Install dependencies
COPY ./requirements/base.txt /code/requirements/
RUN pip install --upgrade pip
RUN pip install -r ./requirements/base.txt
# Copy project
COPY . /code/
\end{verbatim}
\hypertarget{_makefile}{%
\subsubsection{Makefile}\label{_makefile}}
Pour gagner un peu de temps, n'hésitez pas à créer un fichier
\texttt{Makefile} que vous placerez à la racine du projet. L'exemple
ci-dessous permettra, grâce à la commande \texttt{make\ coverage},
d'arriver au même résultat que ci-dessus:
\begin{verbatim}
# Makefile for gwift
#
# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
$(error The 'coverage' command was not found. Make sure you have coverage installed)
endif
.PHONY: help coverage
help:
@echo " coverage to run coverage check of the source files."
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
\end{verbatim}
Pour la petite histoire, \texttt{make} peu sembler un peu désuet, mais
reste extrêmement efficace.
\hypertarget{_environnement_de_duxe9veloppement}{%
\subsection{Environnement de
développement}\label{_environnement_de_duxe9veloppement}}
Concrètement, nous pourrions tout à fait nous limiter à Notepad ou
Notepad++. Mais à moins d'aimer se fouetter avec un câble USB, nous
apprécions la complétion du code, la coloration syntaxique,
l'intégration des tests unitaires et d'un debugger, ainsi que deux-trois
sucreries qui feront plaisir à n'importe quel développeur.
Si vous manquez d'idées ou si vous ne savez pas par où commencer:
\begin{itemize}
\item
\href{https://vscodium.com/}{VSCodium}, avec les plugins
\href{https://marketplace.visualstudio.com/items?itemName=ms-python.python}{Python}et
\href{https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens}{GitLens}
\item
\href{https://www.jetbrains.com/pycharm/}{PyCharm}
\item
\href{https://www.vim.org/}{Vim} avec les plugins
\href{https://github.com/davidhalter/jedi-vim}{Jedi-Vim} et
\href{https://github.com/preservim/nerdtree}{nerdtree}
\end{itemize}
Si vous hésitez, et même si Codium n'est pas le plus léger (la faute à
\href{https://www.electronjs.org/}{Electron}\ldots\hspace{0pt}), il fera
correctement son travail (à savoir: faciliter le vôtre), en intégrant
suffisament de fonctionnalités qui gâteront les papilles émoustillées du
développeur impatient.
\begin{figure}
\centering
\includegraphics{images/environment/codium.png}
\caption{Codium en action}
\end{figure}
\hypertarget{_un_terminal}{%
\subsection{Un terminal}\label{_un_terminal}}
\emph{A priori}, les IDE \footnote{Integrated Development Environment}
proposés ci-dessus fournissent par défaut ou \emph{via} des greffons un
terminal intégré. Ceci dit, disposer d'un terminal séparé facilite
parfois certaines tâches.
A nouveau, si vous manquez d'idées:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Si vous êtes sous Windows, téléchargez une copie de
\href{https://cmder.net/}{Cmder}. Il n'est pas le plus rapide, mais
propose une intégration des outils Unix communs (\texttt{ls},
\texttt{pwd}, \texttt{grep}, \texttt{ssh}, \texttt{git},
\ldots\hspace{0pt}) sans trop se fouler.
\item
Pour tout autre système, vous devriez disposer en natif de ce qu'il
faut.
\end{enumerate}
\begin{figure}
\centering
\includegraphics{images/environment/terminal.png}
\caption{Mise en abîme}
\end{figure}
\hypertarget{_un_gestionnaire_de_base_de_donnuxe9es}{%
\subsection{Un gestionnaire de base de
données}\label{_un_gestionnaire_de_base_de_donnuxe9es}}
Django gère plusieurs moteurs de base de données. Certains sont gérés
nativement par Django (PostgreSQL, MariaDB, SQLite); \emph{a priori},
ces trois-là sont disponibles pour tous les systèmes d'exploitation.
D'autres moteurs nécessitent des librairies tierces (Oracle, Microsoft
SQL Server).
Il n'est pas obligatoire de disposer d'une application de gestion pour
ces moteurs: pour les cas d'utilisation simples, le shell Django pourra
largement suffire (nous y reviendrons). Mais pour faciliter la gestion
des bases de données elles-même, et si vous n'êtes pas à l'aise avec la
ligne de commande, choisissez l'une des applications d'administration
ci-dessous en fonction du moteur de base de données que vous souhaitez
utiliser.
\begin{itemize}
\item
Pour \textbf{PostgreSQL}, il existe
\href{https://www.pgadmin.org/}{pgAdmin}
\item
Pour \textbf{MariaDB} ou \textbf{MySQL}, partez sur
\href{https://www.phpmyadmin.net/}{PHPMyAdmin}
\item
Pour \textbf{SQLite}, il existe
\href{https://sqlitebrowser.org/}{SQLiteBrowser} PHPMyAdmin ou
PgAdmin.
\end{itemize}
\hypertarget{_un_gestionnaire_de_mots_de_passe}{%
\subsection{Un gestionnaire de mots de
passe}\label{_un_gestionnaire_de_mots_de_passe}}
Nous en auront besoin pour gé(né)rer des phrases secrètes pour nos
applications. Si vous n'en utilisez pas déjà un, partez sur
\href{https://keepassxc.org/}{KeepassXC}: il est multi-plateformes,
suivi et s'intègre correctement aux différents environnements, tout en
restant accessible.
\includegraphics{images/environment/keepass.png}
\hypertarget{_un_systuxe8me_de_gestion_de_versions}{%
\subsection{Un système de gestion de
versions}\label{_un_systuxe8me_de_gestion_de_versions}}
Il existe plusieurs systèmes de gestion de versions. Le plus connu à
l'heure actuelle est \href{https://git-scm.com/}{Git}, notamment pour sa
(très) grande flexibilité et sa rapidité d'exécution. Il est une aide
précieuse pour développer rapidement des preuves de concept, switcher
vers une nouvelle fonctionnalité, un bogue à réparer ou une nouvelle
release à proposer au téléchargement. Ses deux plus gros défauts
concerneraient peut-être sa courbe d'apprentissage pour les nouveaux
venus et la complexité des actions qu'il permet de réaliser.
\begin{figure}
\centering
\includegraphics{images/xkcd-1597-git.png}
\caption{\url{https://xkcd.com/1597/}}
\end{figure}
Même pour un développeur solitaire, un système de gestion de versions
(quel qu'il soit) reste indispensable.
Chaque "\textbf{branche}" correspond à une tâche à réaliser: un bogue à
corriger (\emph{Hotfix A}), une nouvelle fonctionnalité à ajouter ou un
"\emph{truc à essayer}" \footnote{Oui, comme dans "Attends, j'essaie
vite un truc, si ça marche, c'est beau."} (\emph{Feature A} et
\emph{Feature B}).
Chaque "\textbf{commit}" correspond à une sauvegarde atomique d'un état
ou d'un ensemble de modifications cohérentes entre elles.\footnote{Il
convient donc de s'abstenir de modifier le CSS d'une application et la
couche d'accès à la base de données, sous peine de se faire huer par
ses relecteurs au prochain stand-up.} De cette manière, il est
beaucoup plus facile pour le développeur de se concenter sur un sujet en
particulier, dans la mesure où celui-ci ne doit pas obligatoirement être
clôturé pour appliquer un changement de contexte.
\begin{figure}
\centering
\includegraphics{images/diagrams/git-workflow.png}
\caption{Git en action}
\end{figure}
Cas pratique: vous développez cette nouvelle fonctionnalité qui va
révolutionner le monde de demain et d'après-demain, quand, tout à coup
(!), vous vous rendez compte que vous avez perdu votre conformité aux
normes PCI parce les données des titulaires de cartes ne sont pas
isolées correctement. Il suffit alors de:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
sauver le travail en cours
(\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ {[}WIP{]}})
\item
revenir sur la branche principale (\texttt{git\ checkout\ main})
\item
créer un "hotfix" (\texttt{git\ checkout\ -b\ hotfix/pci-compliance})
\item
solutionner le problème (sans doute un \texttt{;} en trop ?)
\item
sauver le correctif sur cette branche
(\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ "Did\ it!"})
\item
récupérer ce correctif sur la branche principal
(\texttt{git\ checkout\ main\ \&\&\ git\ merge\ hotfix/pci-compliance})
\item
et revenir tranquillou sur votre branche de développement pour
fignoler ce générateur de noms de dinosaures rigolos que l'univers
vous réclame à cor et à a cri
(\texttt{git\ checkout\ features/dinolol})
\end{enumerate}
Finalement, sachez qu'il existe plusieurs manières de gérer ces flux
d'informations. Les plus connus sont
\href{https://www.gitflow.com/}{Gitflow} et
\href{https://www.reddit.com/r/programming/comments/7mfxo6/a_branching_strategy_simpler_than_gitflow/}{Threeflow}.
\hypertarget{_duxe9crire_ses_changements}{%
\subsubsection{Décrire ses
changements}\label{_duxe9crire_ses_changements}}
La description d'un changement se fait \emph{via} la commande
\texttt{git\ commit}. Il est possible de lui passer directement le
message associé à ce changement grâce à l'attribut \texttt{-m}, mais
c'est une pratique relativement déconseillée: un \emph{commit} ne doit
effectivement pas obligatoirement être décrit sur une seule ligne. Une
description plus complète, accompagnée des éventuels tickets ou
références, sera plus complète, plus agréable à lire, et plus facile à
revoir pour vos éventuels relecteurs.
De plus, la plupart des plateformes de dépôts présenteront ces
informations de manière ergonomique. Par exemple:
\begin{figure}
\centering
\includegraphics{images/environment/gitea-commit-message.png}
\caption{Un exemple de commit affiché dans Gitea}
\end{figure}
La première ligne est reprise comme titre (normalement, sur 50
caractères maximum); le reste est repris comme de la description.
\hypertarget{_un_systuxe8me_de_virtualisation}{%
\subsection{Un système de
virtualisation}\label{_un_systuxe8me_de_virtualisation}}
Par "\emph{système de virtualisation}", nous entendons n'importe quel
application, système d'exploitation, système de containeurisation,
\ldots\hspace{0pt} 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}
\item
\href{https://www.virtualbox.org/}{VirtualBox}
\item
\href{https://www.vagrantup.com/}{Vagrant}
\item
\href{https://www.docker.com/}{Docker}
\item
\href{https://linuxcontainers.org/lxc/}{Linux Containers (LXC)}
\item
\href{https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v}{Hyper-V}
\end{itemize}
Ces quelques propositions se situent un cran plus loin que la "simple"
isolation d'un environnement, puisqu'elles vous permettront de
construire un environnement complet. Elles constituent donc une étape
supplémentaires dans la configuration de votre espace de travail, mais
en amélioreront la qualité.
Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux
solutions automatisables et multiplateformes, dont la configuration peut
faire partie intégrante de vos sources.
\hypertarget{_vagrant}{%
\subsubsection{Vagrant}\label{_vagrant}}
Vagrant consiste en un outil de création et de gestion d'environnements
virtualisés, en respectant toujours une même manière de travailler,
indépendamment des choix techniques et de l'infrastructure que vous
pourriez sélectionner.
\begin{quote}
Vagrant is a tool for building and managing virtual machine environments
in a single workflow. With an easy-to-use workflow and focus on
automation, Vagrant lowers development environment setup time, increases
production parity, and makes the "works on my machine" excuse a relic of
the past. \footnote{\url{https://www.vagrantup.com/intro}}
\end{quote}
La partie la plus importante de la configuration de Vagrant pour votre
projet consiste à placer un fichier \texttt{Vagrantfile} - \emph{a
priori} à la racine de votre projet - et qui contiendra les information
suivantes:
\begin{itemize}
\item
Le choix du \emph{fournisseur} (\textbf{provider}) de virtualisation
(Virtualbox, Hyper-V et Docker sont natifs; il est également possible
de passer par VMWare, AWS, etc.)
\item
Une \emph{box}, qui consiste à lui indiquer le type et la version
attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et
\href{https://app.vagrantup.com/boxes/search}{il y a du choix}).
\item
La manière dont la fourniture (\textbf{provisioning}) de
l'environnement doit être réalisée: scripts Shell, fichiers, Ansible,
Puppet, Chef, \ldots\hspace{0pt} Choisissez votre favori :-) même s'il
est toujours possible de passer par une installation et une
maintenance manuelle, après s'être connecté sur la machine.
\item
Si un espace de stockage doit être partagé entre la machine virtuelle
et l'hôte
\item
Les ports qui doivent être transmis de la machine virtuelle vers
l'hôte.
\end{itemize}
La syntaxe de ce fichier \texttt{Vagrantfile} est en
\href{https://www.ruby-lang.org/en/}{Ruby}. Vous trouverez ci-dessous un
exemple, généré (et nettoyé) après avoir exécuté la commande
\texttt{vagrant\ init}:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# {-}*{-} mode: ruby {-}*{-}}
\CommentTok{\# vi: set ft=ruby :}
\DataTypeTok{Vagrant}\NormalTok{.configure(}\StringTok{"2"}\NormalTok{) }\KeywordTok{do}\NormalTok{ |config|}
\NormalTok{ config.vm.box = }\StringTok{"ubuntu/bionic64"}
\NormalTok{ config.vm.network }\StringTok{"forwarded\_port"}\NormalTok{, }\StringTok{guest: }\DecValTok{80}\NormalTok{, }\StringTok{host: }\DecValTok{8080}\NormalTok{, }\StringTok{host\_ip: "127.0.0.1"}
\NormalTok{ config.vm.provider }\StringTok{"virtualbox"} \KeywordTok{do}\NormalTok{ |vb|}
\NormalTok{ vb.gui = }\DecValTok{true}
\NormalTok{ vb.memory = }\StringTok{"1024"}
\KeywordTok{end}
\NormalTok{ config.vm.provision }\StringTok{"shell"}\NormalTok{, }\StringTok{inline: }\NormalTok{\textless{}\textless{}{-}}\KeywordTok{SHELL}
\OtherTok{ apt{-}get update}
\OtherTok{ apt{-}get install {-}y nginx}
\OtherTok{ }\KeywordTok{SHELL}
\KeywordTok{end}
\end{Highlighting}
\end{Shaded}
Dans le fichier ci-dessus, nous créons:
\begin{itemize}
\item
Une nouvelle machine virtuelle (ie. \emph{invitée}) sous Ubuntu Bionic
Beaver, en x64
\item
Avec une correspondance du port \texttt{80} de la machine vers le port
\texttt{8080} de l'hôte, en limitant l'accès à celui-ci - accédez à
\texttt{localhost:8080} et vous accéderez au port \texttt{80} de la
machine virtuelle.
\item
En utilisant Virtualbox comme backend - la mémoire vive allouée sera
limitée à 1Go de RAM et nous ne voulons pas voir l'interface graphique
au démarrage
\item
Et pour finir, nous voulons appliquer un script de mise à jour
\texttt{apt-get\ update} et installer le paquet \texttt{nginx}
\end{itemize}
Par défaut, le répertoire courant (ie. le répertoire dans lequel notre
fichier \texttt{Vagrantfile} se trouve) sera synchronisé dans le
répertoire \texttt{/vagrant} sur la machine invitée.
\hypertarget{_docker}{%
\subsubsection{Docker}\label{_docker}}
(copié/collé de cookie-cutter-django)
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{version: }\StringTok{\textquotesingle{}3\textquotesingle{}}
\NormalTok{volumes:}
\NormalTok{ local\_postgres\_data: \{\}}
\NormalTok{ local\_postgres\_data\_backups: \{\}}
\NormalTok{services:}
\NormalTok{ django: \&django}
\NormalTok{ build:}
\NormalTok{ context: .}
\NormalTok{ dockerfile: ./compose/local/django/Dockerfile}
\NormalTok{ image: khana\_local\_django}
\NormalTok{ container\_name: django}
\NormalTok{ depends\_on:}
\NormalTok{ {-} postgres}
\NormalTok{ volumes:}
\NormalTok{ {-} .:/app:z}
\NormalTok{ env\_file:}
\NormalTok{ {-} ./.envs/.local/.django}
\NormalTok{ {-} ./.envs/.local/.postgres}
\NormalTok{ ports:}
\NormalTok{ {-} }\StringTok{"8000:8000"}
\NormalTok{ command: /start}
\NormalTok{ postgres:}
\NormalTok{ build:}
\NormalTok{ context: .}
\NormalTok{ dockerfile: ./compose/production/postgres/Dockerfile}
\NormalTok{ image: khana\_production\_postgres}
\NormalTok{ container\_name: postgres}
\NormalTok{ volumes:}
\NormalTok{ {-} local\_postgres\_data:/var/lib/postgresql/data:Z}
\NormalTok{ {-} local\_postgres\_data\_backups:/backups:z}
\NormalTok{ env\_file:}
\NormalTok{ {-} ./.envs/.local/.postgres}
\NormalTok{ docs:}
\NormalTok{ image: khana\_local\_docs}
\NormalTok{ container\_name: docs}
\NormalTok{ build:}
\NormalTok{ context: .}
\NormalTok{ dockerfile: ./compose/local/docs/Dockerfile}
\NormalTok{ env\_file:}
\NormalTok{ {-} ./.envs/.local/.django}
\NormalTok{ volumes:}
\NormalTok{ {-} ./docs:/docs:z}
\NormalTok{ {-} ./config:/app/config:z}
\NormalTok{ {-} ./khana:/app/khana:z}
\NormalTok{ ports:}
\NormalTok{ {-} }\StringTok{"7000:7000"}
\NormalTok{ command: /start{-}docs}
\NormalTok{ redis:}
\NormalTok{ image: redis:5.0}
\NormalTok{ container\_name: redis}
\NormalTok{ celeryworker:}
\NormalTok{ \textless{}\textless{}: *django}
\NormalTok{ image: khana\_local\_celeryworker}
\NormalTok{ container\_name: celeryworker}
\NormalTok{ depends\_on:}
\NormalTok{ {-} redis}
\NormalTok{ {-} postgres}
\NormalTok{ ports: []}
\NormalTok{ command: /start{-}celeryworker}
\NormalTok{ celerybeat:}
\NormalTok{ \textless{}\textless{}: *django}
\NormalTok{ image: khana\_local\_celerybeat}
\NormalTok{ container\_name: celerybeat}
\NormalTok{ depends\_on:}
\NormalTok{ {-} redis}
\NormalTok{ {-} postgres}
\NormalTok{ ports: []}
\NormalTok{ command: /start{-}celerybeat}
\NormalTok{ flower:}
\NormalTok{ \textless{}\textless{}: *django}
\NormalTok{ image: khana\_local\_flower}
\NormalTok{ container\_name: flower}
\NormalTok{ ports:}
\NormalTok{ {-} }\StringTok{"5555:5555"}
\NormalTok{ command: /start{-}flower}
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# docker{-}compose.yml}
\NormalTok{version: }\StringTok{\textquotesingle{}3.8\textquotesingle{}}
\NormalTok{services:}
\NormalTok{ web:}
\NormalTok{ build: .}
\NormalTok{ command: python /code/manage.py runserver 0.0.0.0:8000}
\NormalTok{ volumes:}
\NormalTok{ {-} .:/code}
\NormalTok{ ports:}
\NormalTok{ {-} 8000:8000}
\NormalTok{ depends\_on:}
\NormalTok{ {-} slqserver}
\NormalTok{ slqserver:}
\NormalTok{ image: mcr.microsoft.com/mssql/server:2019{-}latest}
\NormalTok{ environment:}
\NormalTok{ {-} }\StringTok{"ACCEPT\_EULA=Y"}
\NormalTok{ {-} }\StringTok{"SA\_PASSWORD=sqklgjqihagrtdgqk12§!"}
\NormalTok{ ports:}
\NormalTok{ {-} 1433:1433}
\NormalTok{ volumes:}
\NormalTok{ {-} ../sqlserver/data:/var/opt/mssql/data}
\NormalTok{ {-} ../sqlserver/log:/var/opt/mssql/log}
\NormalTok{ {-} ../sqlserver/secrets:/var/opt/mssql/secrets}
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{FROM}\NormalTok{ python:3.8{-}slim{-}buster}
\KeywordTok{ENV}\NormalTok{ PYTHONUNBUFFERED 1}
\KeywordTok{ENV}\NormalTok{ PYTHONDONTWRITEBYTECODE 1}
\KeywordTok{RUN}\NormalTok{ apt{-}get update \textbackslash{}}
\CommentTok{\# dependencies for building Python packages}
\NormalTok{ \&\& apt{-}get install {-}y build{-}essential \textbackslash{}}
\CommentTok{\# psycopg2 dependencies}
\NormalTok{ \&\& apt{-}get install {-}y libpq{-}dev \textbackslash{}}
\CommentTok{\# Translations dependencies}
\NormalTok{ \&\& apt{-}get install {-}y gettext \textbackslash{}}
\CommentTok{\# cleaning up unused files}
\NormalTok{ \&\& apt{-}get purge {-}y {-}{-}auto{-}remove {-}o APT::AutoRemove::RecommendsImportant=false \textbackslash{}}
\NormalTok{ \&\& rm {-}rf /var/lib/apt/lists/*}
\CommentTok{\# Requirements are installed here to ensure they will be cached.}
\KeywordTok{COPY}\NormalTok{ ./requirements /requirements}
\KeywordTok{RUN}\NormalTok{ pip install {-}r /requirements/local.txt}
\KeywordTok{COPY}\NormalTok{ ./compose/production/django/entrypoint /entrypoint}
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /entrypoint}
\KeywordTok{RUN}\NormalTok{ chmod +x /entrypoint}
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/start /start}
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start}
\KeywordTok{RUN}\NormalTok{ chmod +x /start}
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/celery/worker/start /start{-}celeryworker}
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start{-}celeryworker}
\KeywordTok{RUN}\NormalTok{ chmod +x /start{-}celeryworker}
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/celery/beat/start /start{-}celerybeat}
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start{-}celerybeat}
\KeywordTok{RUN}\NormalTok{ chmod +x /start{-}celerybeat}
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/celery/flower/start /start{-}flower}
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start{-}flower}
\KeywordTok{RUN}\NormalTok{ chmod +x /start{-}flower}
\KeywordTok{WORKDIR}\NormalTok{ /app}
\KeywordTok{ENTRYPOINT}\NormalTok{ [}\StringTok{"/entrypoint"}\NormalTok{]}
\end{Highlighting}
\end{Shaded}
Voir comment nous pouvons intégrer toutes ces commandes au niveau de la
CI et au niveau du déploiement (Docker-compose ?)
\hypertarget{_base_de_donnuxe9es}{%
\subsubsection{Base de données}\label{_base_de_donnuxe9es}}
Parfois, SQLite peut être une bonne option:
\begin{quote}
Write througput is the area where SQLite struggles the most, but there's
not a ton of compelling data online about how it fares, so I got some of
my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly
modified1 version of the SQLite kvtest2 program on it. Writing 512 byte
blobs as separate transactions, in WAL mode with synchronous=normal3,
temp\_store=memory, and mmap enabled, I got 13.78μs per write, or
\textasciitilde72,568 writes per second. Going a bit larger, at 32kb
writes, I got 303.74μs per write, or \textasciitilde3,292 writes per
second. That's not astronomical, but it's certainly way more than most
websites being used by humans need. If you had 10 million daily active
users, each one could get more than 600 writes per day with that.
\end{quote}
\begin{quote}
Looking at read throughput, SQLite can go pretty far: with the same test
above, I got a read throughput of \textasciitilde496,770 reads/sec
(2.013μs/read) for the 512 byte blob. Other people also report similar
results --- Expensify reports that you can get 4M QPS if you're willing
to make some slightly more involved changes and use a beefier server.
Four million QPS is enough that every internet user in the world could
make \textasciitilde70 queries per day, with a little headroom left
over4. Most websites don't need that kind of throughput.
cite:{[}consider\_sqlite{]}
\end{quote}
\hypertarget{_duxe9marrer_un_nouveau_projet}{%
\section{Démarrer un nouveau
projet}\label{_duxe9marrer_un_nouveau_projet}}
\hypertarget{_travailler_en_isolation}{%
\subsection{Travailler en isolation}\label{_travailler_en_isolation}}
Nous allons aborder la gestion et l'isolation des dépendances. Cette
section est aussi utile pour une personne travaillant seule, que pour
transmettre les connaissances à un nouveau membre de l'équipe ou pour
déployer l'application elle-même.
Il en était déjà question au deuxième point des 12 facteurs: même dans
le cas de petits projets, il est déconseillé de s'en passer. Cela évite
les déploiements effectués à l'arrache à grand renfort de \texttt{sudo}
et d'installation globale de dépendances, pouvant potentiellement
occasioner des conflits entre les applications déployées:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Il est tout à fait envisagable que deux applications différentes
soient déployées sur un même hôte, et nécessitent chacune deux
versions différentes d'une même dépendance.
\item
Pour la reproductibilité d'un environnement spécifique, cela évite
notamment les réponses type "Ca juste marche chez moi", puisque la
construction d'un nouvel environnement fait partie intégrante du
processus de construction et de la documentation du projet; grâce à
elle, nous avons la possibilité de construire un environnement sain et
d'appliquer des dépendances identiques, quelle que soit la machine
hôte.
\end{enumerate}
\includegraphics{images/it-works-on-my-machine.jpg}
Dans la suite de ce chapitre, nous allons considérer deux projets
différents:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Gwift, une application permettant de gérer des listes de souhaits
\item
Khana, une application de suivi d'apprentissage pour des élèves ou
étudiants.
\end{enumerate}
\hypertarget{_roulements_de_versions}{%
\subsubsection{Roulements de versions}\label{_roulements_de_versions}}
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}).
\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).
\hypertarget{_environnements_virtuels}{%
\subsubsection{Environnements virtuels}\label{_environnements_virtuels}}
\begin{figure}
\centering
\includegraphics{images/xkcd-1987.png}
\caption{\url{https://xkcd.com/1987}}
\end{figure}
Un des reproches que l'on peut faire au langage concerne sa versatilité:
il est possible de réaliser beaucoup de choses, mais celles-ci ne sont
pas toujours simples ou directes. Pour quelqu'un qui débarquererait, la
quantité d'options différentes peut paraître rebutante. Nous pensons
notamment aux environnements virtuels: ils sont géniaux à utiliser, mais
on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version
améliorée et plus ergonomique), \texttt{venv} (la version intégrée
depuis la version 3.3 de l'interpréteur, et
\href{https://docs.python.org/3/library/venv.html}{la manière
recommandée} de créer un environnement depuis la 3.5).
Pour créer un nouvel environnement, vous aurez donc besoin:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
D'une installation de Python - \url{https://www.python.org/}
\item
D'un terminal - voir le point
\href{../environment/_index.xml\#un-terminal}{Un terminal}
\end{enumerate}
Il existe plusieurs autres modules permettant d'arriver au même
résultat, avec quelques avantages et inconvénients pour chacun d'entre
eux. Le plus prometteur d'entre eux est
\href{https://python-poetry.org/}{Poetry}, qui dispose d'une interface
en ligne de commande plus propre et plus moderne que ce que PIP propose.
Poetry se propose de gérer le projet au travers d'un fichier
pyproject.toml. TOML (du nom de son géniteur, Tom Preston-Werner,
légèrement CEO de GitHub à ses heures), se place comme alternative aux
formats comme JSON, YAML ou INI.
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{La}\NormalTok{ commande poetry new }\OperatorTok{\textless{}}\NormalTok{project}\OperatorTok{\textgreater{}}\NormalTok{ créera une structure par défaut relativement compréhensible:}
\NormalTok{$ }\ExtensionTok{poetry}\NormalTok{ new django{-}gecko}
\NormalTok{$ }\ExtensionTok{tree}\NormalTok{ django{-}gecko/}
\ExtensionTok{django{-}gecko/}
\NormalTok{├── }\ExtensionTok{django\_gecko}
\NormalTok{│ └── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{├── }\ExtensionTok{pyproject.toml}
\NormalTok{├── }\ExtensionTok{README.rst}
\NormalTok{└── }\ExtensionTok{tests}
\NormalTok{ ├── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{ └── }\ExtensionTok{test\_django\_gecko.py}
\ExtensionTok{2}\NormalTok{ directories, 5 files}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\FunctionTok{mkdir}\NormalTok{ \textasciitilde{}/.venvs/}
\ExtensionTok{python}\NormalTok{ {-}m venv \textasciitilde{}/.venvs/gwift{-}venv}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\CommentTok{\# GNU/Linux, macOS}
\BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/gwift{-}venv/bin/activate}
\CommentTok{\# MS Windows, avec Cmder}
\ExtensionTok{\textasciitilde{}/.venvs/gwift{-}venv/Scripts/activate.bat}
\CommentTok{\# Pour les deux}
\KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/.venvs/gwift{-}env$ }
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Le terminal signale que nous sommes bien dans l'environnement
\texttt{gwift-env}.
\end{itemize}
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}.
\hypertarget{_python_packaging_made_easy}{%
\subsubsection{Python packaging made
easy}\label{_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}
\def\labelenumi{\arabic{enumi}.}
\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.
\hypertarget{_gestion_des_duxe9pendances_installation_de_django_et_cruxe9ation_dun_nouveau_projet}{%
\subsubsection{Gestion des dépendances, installation de Django et
création d'un nouveau
projet}\label{_gestion_des_duxe9pendances_installation_de_django_et_cruxe9ation_dun_nouveau_projet}}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/gwift{-}env/bin/activate }\CommentTok{\# ou \textasciitilde{}/.venvs/gwift{-}env/Scrips/activate.bat pour Windows.}
\NormalTok{$ }\ExtensionTok{pip}\NormalTok{ install django}
\ExtensionTok{Collecting}\NormalTok{ django}
\ExtensionTok{Downloading}\NormalTok{ Django{-}3.1.4}
\ExtensionTok{100\%} \KeywordTok{|}\NormalTok{\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#}\KeywordTok{|}
\ExtensionTok{Installing}\NormalTok{ collected packages: django}
\ExtensionTok{Successfully}\NormalTok{ installed django{-}3.1.4}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{django{-}admin}\NormalTok{ startproject gwift}
\end{Highlighting}
\end{Shaded}
Cette action a pour effet de créer un nouveau dossier \texttt{gwift},
dans lequel nous trouvons la structure suivante:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{tree}\NormalTok{ gwift}
\ExtensionTok{gwift}
\NormalTok{├── }\ExtensionTok{gwift}
\KeywordTok{|} \KeywordTok{|}\NormalTok{── }\ExtensionTok{asgi.py}
\NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   ├── }\ExtensionTok{settings.py}
\NormalTok{│   ├── }\ExtensionTok{urls.py}
\NormalTok{│   └── }\ExtensionTok{wsgi.py}
\NormalTok{└── }\ExtensionTok{manage.py}
\end{Highlighting}
\end{Shaded}
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:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{(}\ExtensionTok{gwift}\KeywordTok{)}\NormalTok{ $ }\FunctionTok{mkdir}\NormalTok{ requirements}
\KeywordTok{(}\ExtensionTok{gwift}\KeywordTok{)}\NormalTok{ $ }\FunctionTok{touch}\NormalTok{ README.md}
\KeywordTok{(}\ExtensionTok{gwift}\KeywordTok{)}\NormalTok{ $ }\ExtensionTok{tree}\NormalTok{ gwift}
\ExtensionTok{gwift}
\NormalTok{├── }\ExtensionTok{gwift}
\NormalTok{│   ├── }\ExtensionTok{asgi.py}
\NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   ├── }\ExtensionTok{settings.py}
\NormalTok{│   ├── }\ExtensionTok{urls.py}
\NormalTok{│   └── }\ExtensionTok{wsgi.py}
\NormalTok{├── }\ExtensionTok{requirements}
\NormalTok{├── }\ExtensionTok{README.md}
\NormalTok{└── }\ExtensionTok{manage.py}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Ici
\item
Et là
\end{itemize}
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 strictement inférieure à la version 3.2 dans le fichier
\texttt{requirements/base.txt}.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\BuiltInTok{echo} \StringTok{\textquotesingle{}django==3.2\textquotesingle{}} \OperatorTok{\textgreater{}}\NormalTok{ requirements/base.txt}
\NormalTok{$ }\BuiltInTok{echo} \StringTok{\textquotesingle{}{-}r base.txt\textquotesingle{}} \OperatorTok{\textgreater{}}\NormalTok{ requirements/prod.txt}
\NormalTok{$ }\BuiltInTok{echo} \StringTok{\textquotesingle{}{-}r base.txt\textquotesingle{}} \OperatorTok{\textgreater{}}\NormalTok{ requirements/dev.txt}
\end{Highlighting}
\end{Shaded}
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 parcourir
les 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.
\hypertarget{_gestion_des_diffuxe9rentes_versions_des_python}{%
\subsection{Gestion des différentes versions des
Python}\label{_gestion_des_diffuxe9rentes_versions_des_python}}
\begin{verbatim}
pyenv install 3.10
\end{verbatim}
\hypertarget{_django}{%
\subsection{Django}\label{_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}
\def\labelenumi{\arabic{enumi}.}
\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},
\ldots\hspace{0pt}).
\hypertarget{_manage_py}{%
\subsubsection{manage.py}\label{_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, \ldots\hspace{0pt}
\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{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py runserver}
\ExtensionTok{Watching}\NormalTok{ for file changes with StatReloader}
\ExtensionTok{Performing}\NormalTok{ system checks...}
\ExtensionTok{System}\NormalTok{ check identified no issues (0 silenced)}\ExtensionTok{.}
\NormalTok{[}\ExtensionTok{...}\NormalTok{]}
\ExtensionTok{December}\NormalTok{ 15, 2020 {-} 20:45:07}
\ExtensionTok{Django}\NormalTok{ version 3.1.4, using settings }\StringTok{\textquotesingle{}gwift.settings\textquotesingle{}}
\ExtensionTok{Starting}\NormalTok{ development server at http://127.0.0.1:8000/}
\ExtensionTok{Quit}\NormalTok{ the server with CTRL{-}BREAK.}
\end{Highlighting}
\end{Shaded}
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.
\hypertarget{_cruxe9ation_dune_nouvelle_application}{%
\subsubsection{Création d'une nouvelle
application}\label{_cruxe9ation_dune_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{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py startapp wish}
\end{Highlighting}
\end{Shaded}
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:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/gwift$ tree .}
\ExtensionTok{.}
\NormalTok{├── }\ExtensionTok{gwift}
\NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   ├── }\ExtensionTok{asgi.py}
\NormalTok{│   ├── }\ExtensionTok{settings.py}
\NormalTok{│   ├── }\ExtensionTok{urls.py}
\NormalTok{│   ├── }\ExtensionTok{wish}
\NormalTok{│   │   ├── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   │   ├── }\ExtensionTok{admin.py}
\NormalTok{│   │   ├── }\ExtensionTok{apps.py}
\NormalTok{│   │   ├── }\ExtensionTok{migrations}
\NormalTok{│   │   │   └── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   │   ├── }\ExtensionTok{models.py}
\NormalTok{│   │   ├── }\ExtensionTok{tests.py}
\NormalTok{│   │   └── }\ExtensionTok{views.py}
\NormalTok{│   └── }\ExtensionTok{wsgi.py}
\NormalTok{├── }\ExtensionTok{Makefile}
\NormalTok{├── }\ExtensionTok{manage.py}
\NormalTok{├── }\ExtensionTok{README.md}
\NormalTok{├── }\ExtensionTok{requirements}
\NormalTok{│   ├── }\ExtensionTok{base.txt}
\NormalTok{│   ├── }\ExtensionTok{dev.txt}
\NormalTok{│   └── }\ExtensionTok{prod.txt}
\NormalTok{├── }\ExtensionTok{setup.cfg}
\NormalTok{└── }\ExtensionTok{tox.ini}
\ExtensionTok{5}\NormalTok{ directories, 22 files}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Notre application a bien été créée, et nous l'avons déplacée dans le
répertoire \texttt{gwift} !
\end{itemize}
\hypertarget{_fonctionement_guxe9nuxe9ral}{%
\subsubsection{Fonctionement
général}\label{_fonctionement_guxe9nuxe9ral}}
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, \ldots\hspace{0pt} 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{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path}
\ImportTok{from}\NormalTok{ gwift.views }\ImportTok{import}\NormalTok{ wish\_details }
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls),}
\NormalTok{ path(}\StringTok{"wishes/\textless{}int:wish\_id\textgreater{}"}\NormalTok{, wish\_details), }
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\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{Shaded}
\begin{Highlighting}[]
\NormalTok{[...]}
\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ datetime}
\KeywordTok{def}\NormalTok{ wishes\_details(request: HttpRequest, wish\_id: }\BuiltInTok{int}\NormalTok{) }\OperatorTok{{-}\textgreater{}}\NormalTok{ HttpResponse:}
\NormalTok{ context }\OperatorTok{=}\NormalTok{ \{}
\StringTok{"user\_name"}\NormalTok{: }\StringTok{"Bond,"}
\StringTok{"user\_first\_name"}\NormalTok{: }\StringTok{"James"}\NormalTok{,}
\StringTok{"now"}\NormalTok{: datetime.now()}
\NormalTok{ \}}
\ControlFlowTok{return}\NormalTok{ render(}
\NormalTok{ request,}
\StringTok{"wish\_details.html"}\NormalTok{,}
\NormalTok{ context}
\NormalTok{ )}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\CommentTok{\textless{}!{-}{-} fichier wish\_details.html {-}{-}\textgreater{}}
\DataTypeTok{\textless{}!DOCTYPE }\NormalTok{html}\DataTypeTok{\textgreater{}}
\KeywordTok{\textless{}html\textgreater{}}
\KeywordTok{\textless{}head\textgreater{}}
\KeywordTok{\textless{}title\textgreater{}}\NormalTok{Page title}\KeywordTok{\textless{}/title\textgreater{}}
\KeywordTok{\textless{}/head\textgreater{}}
\KeywordTok{\textless{}body\textgreater{}}
\KeywordTok{\textless{}h1\textgreater{}}\NormalTok{👤 Hi!}\KeywordTok{\textless{}/h1\textgreater{}}
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{My name is \{\{ user\_name \}\}. \{\{ user\_first\_name \}\} \{\{ user\_name \}\}.}\KeywordTok{\textless{}/p\textgreater{}}
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{This page was generated at \{\{ now \}\}}\KeywordTok{\textless{}/p\textgreater{}}
\KeywordTok{\textless{}/body\textgreater{}}
\KeywordTok{\textless{}/html\textgreater{}}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\DataTypeTok{\textless{}!DOCTYPE }\NormalTok{html}\DataTypeTok{\textgreater{}}
\KeywordTok{\textless{}html\textgreater{}}
\KeywordTok{\textless{}head\textgreater{}}
\KeywordTok{\textless{}title\textgreater{}}\NormalTok{Page title}\KeywordTok{\textless{}/title\textgreater{}}
\KeywordTok{\textless{}/head\textgreater{}}
\KeywordTok{\textless{}body\textgreater{}}
\KeywordTok{\textless{}h1\textgreater{}}\NormalTok{👤 Hi!}\KeywordTok{\textless{}/h1\textgreater{}}
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{My name is Bond. James Bond.}\KeywordTok{\textless{}/p\textgreater{}}
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{This page was generated at 2027{-}03{-}19 19:47:38}\KeywordTok{\textless{}/p\textgreater{}}
\KeywordTok{\textless{}/body\textgreater{}}
\KeywordTok{\textless{}/html\textgreater{}}
\end{Highlighting}
\end{Shaded}
\begin{figure}
\centering
\includegraphics{images/django/django-first-template.png}
\caption{Résultat}
\end{figure}
\hypertarget{_12_facteurs_et_configuration_globale}{%
\subsubsection{12 facteurs et configuration
globale}\label{_12_facteurs_et_configuration_globale}}
→ Faire le lien avec les settings → Faire le lien avec les douze
facteurs → Construction du fichier setup.cfg
\hypertarget{_setup_cfg}{%
\subsubsection{setup.cfg}\label{_setup_cfg}}
(Repris de cookie-cutter-django)
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{[flake8]}
\DataTypeTok{max{-}line{-}length }\OtherTok{=}\StringTok{ }\DecValTok{120}
\DataTypeTok{exclude }\OtherTok{=}\StringTok{ .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node\_modules,venv}
\KeywordTok{[pycodestyle]}
\DataTypeTok{max{-}line{-}length }\OtherTok{=}\StringTok{ }\DecValTok{120}
\DataTypeTok{exclude }\OtherTok{=}\StringTok{ .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node\_modules,venv}
\KeywordTok{[mypy]}
\DataTypeTok{python\_version }\OtherTok{=}\StringTok{ }\FloatTok{3.8}
\DataTypeTok{check\_untyped\_defs }\OtherTok{=}\StringTok{ }\KeywordTok{True}
\DataTypeTok{ignore\_missing\_imports }\OtherTok{=}\StringTok{ }\KeywordTok{True}
\DataTypeTok{warn\_unused\_ignores }\OtherTok{=}\StringTok{ }\KeywordTok{True}
\DataTypeTok{warn\_redundant\_casts }\OtherTok{=}\StringTok{ }\KeywordTok{True}
\DataTypeTok{warn\_unused\_configs }\OtherTok{=}\StringTok{ }\KeywordTok{True}
\DataTypeTok{plugins }\OtherTok{=}\StringTok{ mypy\_django\_plugin.main}
\KeywordTok{[mypy.plugins.django{-}stubs]}
\DataTypeTok{django\_settings\_module }\OtherTok{=}\StringTok{ config.settings.test}
\KeywordTok{[mypy{-}*.migrations.*]}
\CommentTok{\# Django migrations should not produce any errors:}
\DataTypeTok{ignore\_errors }\OtherTok{=}\StringTok{ }\KeywordTok{True}
\KeywordTok{[coverage:run]}
\DataTypeTok{include }\OtherTok{=}\StringTok{ khana/*}
\DataTypeTok{omit }\OtherTok{=}\StringTok{ *migrations*, *tests*}
\DataTypeTok{plugins }\OtherTok{=}
\DataTypeTok{ django\_coverage\_plugin}
\end{Highlighting}
\end{Shaded}
\hypertarget{_structure_finale_de_notre_environnement}{%
\subsection{Structure finale de notre
environnement}\label{_structure_finale_de_notre_environnement}}
Nous avons donc la structure finale pour notre environnement de travail:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/gwift$ tree .}
\ExtensionTok{.}
\NormalTok{├── }\ExtensionTok{gwift}
\NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   ├── }\ExtensionTok{asgi.py}
\NormalTok{│   ├── }\ExtensionTok{settings.py}
\NormalTok{│   ├── }\ExtensionTok{urls.py}
\NormalTok{│   ├── }\ExtensionTok{wish}
\NormalTok{│   │   ├── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   │   ├── }\ExtensionTok{admin.py}
\NormalTok{│   │   ├── }\ExtensionTok{apps.py}
\NormalTok{│   │   ├── }\ExtensionTok{migrations}
\NormalTok{│   │   │   └── }\ExtensionTok{\_\_init\_\_.py}
\NormalTok{│   │   ├── }\ExtensionTok{models.py}
\NormalTok{│   │   ├── }\ExtensionTok{tests.py}
\NormalTok{│   │   └── }\ExtensionTok{views.py}
\NormalTok{│   └── }\ExtensionTok{wsgi.py}
\NormalTok{├── }\ExtensionTok{Makefile}
\NormalTok{├── }\ExtensionTok{manage.py}
\NormalTok{├── }\ExtensionTok{README.md}
\NormalTok{├── }\ExtensionTok{requirements}
\NormalTok{│   ├── }\ExtensionTok{base.txt}
\NormalTok{│   ├── }\ExtensionTok{dev.txt}
\NormalTok{│   └── }\ExtensionTok{prod.txt}
\NormalTok{├── }\ExtensionTok{setup.cfg}
\NormalTok{└── }\ExtensionTok{tox.ini}
\end{Highlighting}
\end{Shaded}
\hypertarget{_cookie_cutter}{%
\subsection{Cookie cutter}\label{_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, \ldots\hspace{0pt}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{python}\NormalTok{ {-}m venv .venvs\textbackslash{}cookie{-}cutter{-}khana}
\NormalTok{λ }\ExtensionTok{.venvs}\NormalTok{\textbackslash{}cookie{-}cutter{-}khana\textbackslash{}Scripts\textbackslash{}activate.bat}
\KeywordTok{(}\ExtensionTok{cookie{-}cutter{-}khana}\KeywordTok{)}\NormalTok{ λ }\ExtensionTok{pip}\NormalTok{ install cookiecutter}
\ExtensionTok{Collecting}\NormalTok{ cookiecutter}
\NormalTok{ [}\ExtensionTok{...}\NormalTok{]}
\ExtensionTok{Successfully}\NormalTok{ 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}
\KeywordTok{(}\ExtensionTok{cookie{-}cutter{-}khana}\KeywordTok{)}\NormalTok{ λ }\ExtensionTok{cookiecutter}\NormalTok{ https://github.com/pydanny/cookiecutter{-}django}
\NormalTok{ [}\ExtensionTok{...}\NormalTok{]}
\NormalTok{ [}\ExtensionTok{SUCCESS}\NormalTok{]: Project initialized, keep up the good work!}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\ExtensionTok{django{-}admin.py}\NormalTok{ startproject {-}{-}template=https://[...].zip }\OperatorTok{\textless{}}\NormalTok{my\_project}\OperatorTok{\textgreater{}}
\end{Highlighting}
\end{Shaded}
Dans ce chapitre, 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.
\hypertarget{_moduxe9lisation}{%
\section{Modélisation}\label{_moduxe9lisation}}
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}
\def\labelenumi{\arabic{enumi}.}
\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.
\hypertarget{_active_records}{%
\subsection{Active Records}\label{_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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Square:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, side):}
\VariableTok{self}\NormalTok{.top\_left }\OperatorTok{=}\NormalTok{ top\_left}
\VariableTok{self}\NormalTok{.side }\OperatorTok{=}\NormalTok{ side}
\KeywordTok{class}\NormalTok{ Rectangle:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, height, width):}
\VariableTok{self}\NormalTok{.top\_left }\OperatorTok{=}\NormalTok{ top\_left}
\VariableTok{self}\NormalTok{.height }\OperatorTok{=}\NormalTok{ height}
\VariableTok{self}\NormalTok{.width }\OperatorTok{=}\NormalTok{ width}
\KeywordTok{class}\NormalTok{ Circle:}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, center, radius):}
\VariableTok{self}\NormalTok{.center }\OperatorTok{=}\NormalTok{ center}
\VariableTok{self}\NormalTok{.radius }\OperatorTok{=}\NormalTok{ radius}
\end{Highlighting}
\end{Shaded}
Si nous souhaitons ajouter une fonctionnalité permettant de calculer
l'aire pour chacune de ces structures, nous aurons deux possibilités:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Geometry:}
\NormalTok{ PI }\OperatorTok{=} \FloatTok{3.141592653589793}
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{, shape):}
\ControlFlowTok{if} \BuiltInTok{isinstance}\NormalTok{(shape, Square):}
\ControlFlowTok{return}\NormalTok{ shape.side }\OperatorTok{*}\NormalTok{ shape.side}
\ControlFlowTok{if} \BuiltInTok{isinstance}\NormalTok{(shape, Rectangle):}
\ControlFlowTok{return}\NormalTok{ shape.height }\OperatorTok{*}\NormalTok{ shape.width}
\ControlFlowTok{if} \BuiltInTok{isinstance}\NormalTok{(shape, Circle):}
\ControlFlowTok{return}\NormalTok{ PI }\OperatorTok{*}\NormalTok{ shape.radius}\OperatorTok{**}\DecValTok{2}
\ControlFlowTok{raise}\NormalTok{ NoSuchShapeException()}
\end{Highlighting}
\end{Shaded}
Dans le second cas, l'implémentation pourrait évoluer de la manière
suivante:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Shape:}
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{pass}
\KeywordTok{class}\NormalTok{ Square(Shape):}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, side):}
\VariableTok{self}\NormalTok{.\_\_top\_left }\OperatorTok{=}\NormalTok{ top\_left}
\VariableTok{self}\NormalTok{.\_\_side }\OperatorTok{=}\NormalTok{ side}
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.\_\_side }\OperatorTok{*} \VariableTok{self}\NormalTok{.\_\_side}
\KeywordTok{class}\NormalTok{ Rectangle(Shape):}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, height, width):}
\VariableTok{self}\NormalTok{.\_\_top\_left }\OperatorTok{=}\NormalTok{ top\_left}
\VariableTok{self}\NormalTok{.\_\_height }\OperatorTok{=}\NormalTok{ height}
\VariableTok{self}\NormalTok{.\_\_width }\OperatorTok{=}\NormalTok{ width}
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.\_\_height }\OperatorTok{*} \VariableTok{self}\NormalTok{.\_\_width}
\KeywordTok{class}\NormalTok{ Circle(Shape):}
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, center, radius):}
\VariableTok{self}\NormalTok{.\_\_center }\OperatorTok{=}\NormalTok{ center}
\VariableTok{self}\NormalTok{.\_\_radius }\OperatorTok{=}\NormalTok{ radius}
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
\NormalTok{ PI }\OperatorTok{=} \FloatTok{3.141592653589793}
\ControlFlowTok{return}\NormalTok{ PI }\OperatorTok{*} \VariableTok{self}\NormalTok{.\_\_radius}\OperatorTok{**}\DecValTok{2}
\end{Highlighting}
\end{Shaded}
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:{[}clean\_code(95-97){]}, 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()},
\ldots\hspace{0pt} En résumé, toutes les méthodes qui font qu'une
instance sait \textbf{comment} interagir avec la base de données.
\hypertarget{_types_de_champs_relations_et_cluxe9s_uxe9tranguxe8res}{%
\subsection{Types de champs, relations et clés
étrangères}\label{_types_de_champs_relations_et_cluxe9s_uxe9tranguxe8res}}
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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Category(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Book(models.Model):}
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ForeignKey(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ library.models }\ImportTok{import}\NormalTok{ Book, Category}
\NormalTok{movies }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Adaptations au cinéma"}\NormalTok{)}
\NormalTok{medieval }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Médiéval{-}Fantastique"}\NormalTok{)}
\NormalTok{science\_fiction }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Sciences{-}fiction"}\NormalTok{)}
\NormalTok{computers }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Sciences Informatiques"}\NormalTok{)}
\NormalTok{books }\OperatorTok{=}\NormalTok{ \{}
\StringTok{"Harry Potter"}\NormalTok{: movies,}
\StringTok{"The Great Gatsby"}\NormalTok{: movies,}
\StringTok{"Dune"}\NormalTok{: science\_fiction,}
\StringTok{"H2G2"}\NormalTok{: science\_fiction,}
\StringTok{"Ender\textquotesingle{}s Game"}\NormalTok{: science\_fiction,}
\StringTok{"Le seigneur des anneaux"}\NormalTok{: medieval,}
\StringTok{"L\textquotesingle{}Assassin Royal"}\NormalTok{, medieval,}
\StringTok{"Clean code"}\NormalTok{: computers,}
\StringTok{"Designing Data{-}Intensive Applications"}\NormalTok{: computers}
\NormalTok{\}}
\ControlFlowTok{for}\NormalTok{ book\_title, category }\KeywordTok{in}\NormalTok{ books.items:}
\NormalTok{ Book.objects.create(name}\OperatorTok{=}\NormalTok{book\_title, category}\OperatorTok{=}\NormalTok{category)}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Category(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Book(models.Model):}
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ManyManyField(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\end{Highlighting}
\end{Shaded}
Notre code d'initialisation reste par contre identique: Django s'occupe
parfaitement de gérer la transition.
\hypertarget{_accuxe8s_aux_relations}{%
\subsubsection{Accès aux relations}\label{_accuxe8s_aux_relations}}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# wish/models.py}
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
\ControlFlowTok{pass}
\KeywordTok{class}\NormalTok{ Item(models.Model):}
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ models.ForeignKey(Wishlist)}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\CommentTok{\# wish/models.py}
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
\ControlFlowTok{pass}
\KeywordTok{class}\NormalTok{ Item(models.Model):}
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ models.ForeignKey(Wishlist, related\_name}\OperatorTok{=}\StringTok{\textquotesingle{}items\textquotesingle{}}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\CommentTok{\# python manage.py shell}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}} \ImportTok{from}\NormalTok{ wish.models }\ImportTok{import}\NormalTok{ Wishlist, Item}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist.create(}\StringTok{\textquotesingle{}Liste de test\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{)}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ item }\OperatorTok{=}\NormalTok{ Item.create(}\StringTok{\textquotesingle{}Element de test\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{, w)}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ item.wishlist}
\OperatorTok{\textless{}}\NormalTok{Wishlist: Wishlist }\BuiltInTok{object}\OperatorTok{\textgreater{}}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ wishlist.items.}\BuiltInTok{all}\NormalTok{()}
\NormalTok{[}\OperatorTok{\textless{}}\NormalTok{Item: Item }\BuiltInTok{object}\OperatorTok{\textgreater{}}\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_n1_queries}{%
\subsubsection{N+1 Queries}\label{_n1_queries}}
\hypertarget{_unicituxe9}{%
\subsection{Unicité}\label{_unicituxe9}}
\hypertarget{_indices}{%
\subsection{Indices}\label{_indices}}
\hypertarget{_conclusions}{%
\subsubsection{Conclusions}\label{_conclusions}}
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.
\hypertarget{_metamoduxe8le_et_introspection}{%
\subsubsection{Metamodèle et
introspection}\label{_metamoduxe8le_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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Wish(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ ordering }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}name\textquotesingle{}}\NormalTok{,) }
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
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.
\end{itemize}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{ constraints }\OperatorTok{=}\NormalTok{ [ }\CommentTok{\# constraints added}
\NormalTok{ models.CheckConstraint(check}\OperatorTok{=}\NormalTok{models.Q(year\_born\_\_lte}\OperatorTok{=}\NormalTok{datetime.date.today().year}\OperatorTok{{-}}\DecValTok{18}\NormalTok{), name}\OperatorTok{=}\StringTok{\textquotesingle{}will\_be\_of\_age\textquotesingle{}}\NormalTok{),}
\NormalTok{ ]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_choix}{%
\subsubsection{Choix}\label{_choix}}
Voir \href{https://girlthatlovestocode.com/django-model}{ici}
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Runner(models.Model):}
\CommentTok{\# this is new:}
\KeywordTok{class}\NormalTok{ Zone(models.IntegerChoices):}
\NormalTok{ ZONE\_1 }\OperatorTok{=} \DecValTok{1}\NormalTok{, }\StringTok{\textquotesingle{}Less than 3.10\textquotesingle{}}
\NormalTok{ ZONE\_2 }\OperatorTok{=} \DecValTok{2}\NormalTok{, }\StringTok{\textquotesingle{}Less than 3.25\textquotesingle{}}
\NormalTok{ ZONE\_3 }\OperatorTok{=} \DecValTok{3}\NormalTok{, }\StringTok{\textquotesingle{}Less than 3.45\textquotesingle{}}
\NormalTok{ ZONE\_4 }\OperatorTok{=} \DecValTok{4}\NormalTok{, }\StringTok{\textquotesingle{}Less than 4 hours\textquotesingle{}}
\NormalTok{ ZONE\_5 }\OperatorTok{=} \DecValTok{5}\NormalTok{, }\StringTok{\textquotesingle{}More than 4 hours\textquotesingle{}}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)}
\NormalTok{ last\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)}
\NormalTok{ email }\OperatorTok{=}\NormalTok{ models.EmailField()}
\BuiltInTok{id} \OperatorTok{=}\NormalTok{ models.UUIDField(primary\_key}\OperatorTok{=}\VariableTok{True}\NormalTok{, default}\OperatorTok{=}\NormalTok{uuid.uuid4, editable}\OperatorTok{=}\VariableTok{False}\NormalTok{)}
\NormalTok{ start\_zone }\OperatorTok{=}\NormalTok{ models.PositiveSmallIntegerField(choices}\OperatorTok{=}\NormalTok{Zone.choices, default}\OperatorTok{=}\NormalTok{Zone.ZONE\_5, help\_text}\OperatorTok{=}\StringTok{"What was your best time on the marathon in last 2 years?"}\NormalTok{) }\CommentTok{\# this is new}
\end{Highlighting}
\end{Shaded}
\hypertarget{_validateurs}{%
\subsubsection{Validateurs}\label{_validateurs}}
\hypertarget{_constructeurs}{%
\subsubsection{Constructeurs}\label{_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:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(name, description):}
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist()}
\NormalTok{ w.name }\OperatorTok{=}\NormalTok{ name}
\NormalTok{ w.description }\OperatorTok{=}\NormalTok{ description}
\NormalTok{ w.save()}
\ControlFlowTok{return}\NormalTok{ w}
\KeywordTok{class}\NormalTok{ Item(models.Model):}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(name, description, wishlist):}
\NormalTok{ i }\OperatorTok{=}\NormalTok{ Item()}
\NormalTok{ i.name }\OperatorTok{=}\NormalTok{ name}
\NormalTok{ i.description }\OperatorTok{=}\NormalTok{ description}
\NormalTok{ i.wishlist }\OperatorTok{=}\NormalTok{ wishlist}
\NormalTok{ i.save()}
\ControlFlowTok{return}\NormalTok{ i}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ ItemManager(...):}
\NormalTok{ (de mémoire, je ne sais plus exactement :}\OperatorTok{{-}}\NormalTok{))}
\end{Highlighting}
\end{Shaded}
\hypertarget{_conclusion_2}{%
\subsection{Conclusion}\label{_conclusion_2}}
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.
Une possibilité peut également être de cantonner Django à un framework
\hypertarget{_querysets_managers}{%
\subsection{Querysets \& managers}\label{_querysets_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 \ldots\hspace{0pt} 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{Shaded}
\begin{Highlighting}[]
\NormalTok{queryset }\OperatorTok{=}\NormalTok{ Wishlist.objects.}\BuiltInTok{all}\NormalTok{()}
\BuiltInTok{print}\NormalTok{(queryset.query)}
\end{Highlighting}
\end{Shaded}
Conditions AND et OR sur un queryset
\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{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ core.models }\ImportTok{import}\NormalTok{ Wish}
\NormalTok{Wish.objects }
\NormalTok{Wish.objects.}\BuiltInTok{filter}\NormalTok{(name\_\_icontains}\OperatorTok{=}\StringTok{"test"}\NormalTok{).}\BuiltInTok{filter}\NormalTok{(name\_\_icontains}\OperatorTok{=}\StringTok{"too"}\NormalTok{) }
\end{Highlighting}
\end{Shaded}
\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{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.db.models }\ImportTok{import}\NormalTok{ Q}
\NormalTok{condition1 }\OperatorTok{=}\NormalTok{ Q(...)}
\NormalTok{condition2 }\OperatorTok{=}\NormalTok{ Q(...)}
\NormalTok{bidule.objects.}\BuiltInTok{filter}\NormalTok{(condition1 }\OperatorTok{|}\NormalTok{ condition2)}
\end{Highlighting}
\end{Shaded}
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;
Ajouter les sujets suivants :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Prefetch
\item
select\_related
\end{enumerate}
\hypertarget{_gestionnaire_de_models_managers_et_opuxe9rations}{%
\subsubsection{Gestionnaire de models (managers) et
opérations}\label{_gestionnaire_de_models_managers_et_opuxe9rations}}
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}
\hypertarget{_requuxeates}{%
\subsubsection{Requêtes}\label{_requuxeates}}
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.
\hypertarget{_jointures}{%
\subsubsection{Jointures}\label{_jointures}}
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{}\hspace{0pt})}. 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\ldots\hspace{0pt}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{informations }\OperatorTok{=}\NormalTok{ (}
\OperatorTok{\textless{}}\NormalTok{MyObject}\OperatorTok{\textgreater{}}\NormalTok{.objects.}\BuiltInTok{filter}\NormalTok{(}\OperatorTok{\textless{}}\NormalTok{my\_criteria}\OperatorTok{\textgreater{}}\NormalTok{)}
\NormalTok{ .select\_related(}\OperatorTok{\textless{}}\NormalTok{related\_field}\OperatorTok{\textgreater{}}\NormalTok{)}
\NormalTok{ .prefetch\_related(}\OperatorTok{\textless{}}\NormalTok{related\_field}\OperatorTok{\textgreater{}}\NormalTok{)}
\NormalTok{ .iterator(chunk\_size}\OperatorTok{=}\DecValTok{1000}\NormalTok{)}
\NormalTok{)}
\end{Highlighting}
\end{Shaded}
\hypertarget{_aggregate_vs_annotate}{%
\subsection{Aggregate vs. Annotate}\label{_aggregate_vs_annotate}}
\url{https://docs.djangoproject.com/en/3.1/topics/db/aggregation/}
\hypertarget{_migrations}{%
\section{Migrations}\label{_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. 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{Shaded}
\begin{Highlighting}[]
\KeywordTok{ALTER} \KeywordTok{TABLE}\NormalTok{ WishList }\KeywordTok{ADD} \KeywordTok{COLUMN}\NormalTok{ Description }\DataTypeTok{nvarchar}\NormalTok{(}\FunctionTok{MAX}\NormalTok{);}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\CommentTok{{-}{-} Firebird}
\KeywordTok{ALTER} \KeywordTok{TABLE} \KeywordTok{Category} \KeywordTok{ALTER} \KeywordTok{COLUMN}\NormalTok{ Name }\KeywordTok{type} \DataTypeTok{varchar}\NormalTok{(}\DecValTok{2000}\NormalTok{)}
\CommentTok{{-}{-} MSSQL}
\KeywordTok{ALTER} \KeywordTok{TABLE} \KeywordTok{Category} \KeywordTok{ALTER} \KeywordTok{Column}\NormalTok{ Name }\DataTypeTok{varchar}\NormalTok{(}\DecValTok{2000}\NormalTok{)}
\CommentTok{{-}{-} Oracle}
\KeywordTok{ALTER} \KeywordTok{TABLE} \KeywordTok{Category} \KeywordTok{MODIFY}\NormalTok{ Name }\DataTypeTok{varchar2}\NormalTok{(}\DecValTok{2000}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
En bref, les problèmes suivants apparaissent très rapidement:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\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}
\hypertarget{_fonctionement_guxe9nuxe9ral_2}{%
\subsection{Fonctionement
général}\label{_fonctionement_guxe9nuxe9ral_2}}
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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Category(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Book(models.Model):}
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ForeignKey(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\end{Highlighting}
\end{Shaded}
Nous avions ensuite modifié la clé de liaison, pour permettre d'associer
plusieurs catégories à un même livre, et inversément:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Category(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Book(models.Model):}
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ManyManyField(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\CommentTok{\# library/migrations/0001\_initial.py}
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
\ImportTok{import}\NormalTok{ django.db.models.deletion}
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
\NormalTok{ initial }\OperatorTok{=} \VariableTok{True}
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ []}
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ migrations.CreateModel( }
\NormalTok{ name}\OperatorTok{=}\StringTok{"Category"}\NormalTok{,}
\NormalTok{ fields}\OperatorTok{=}\NormalTok{[}
\NormalTok{ (}
\StringTok{"id"}\NormalTok{,}
\NormalTok{ models.BigAutoField(}
\NormalTok{ auto\_created}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
\NormalTok{ primary\_key}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
\NormalTok{ serialize}\OperatorTok{=}\VariableTok{False}\NormalTok{,}
\NormalTok{ verbose\_name}\OperatorTok{=}\StringTok{"ID"}\NormalTok{,}
\NormalTok{ ),}
\NormalTok{ ),}
\NormalTok{ (}\StringTok{"name"}\NormalTok{, models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)),}
\NormalTok{ ],}
\NormalTok{ ),}
\NormalTok{ migrations.CreateModel( }
\NormalTok{ name}\OperatorTok{=}\StringTok{"Book"}\NormalTok{,}
\NormalTok{ fields}\OperatorTok{=}\NormalTok{[}
\NormalTok{ (}
\StringTok{"id"}\NormalTok{,}
\NormalTok{ models.BigAutoField(}
\NormalTok{ auto\_created}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
\NormalTok{ primary\_key}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
\NormalTok{ serialize}\OperatorTok{=}\VariableTok{False}\NormalTok{,}
\NormalTok{ verbose\_name}\OperatorTok{=}\StringTok{"ID"}\NormalTok{,}
\NormalTok{ ),}
\NormalTok{ ),}
\NormalTok{ (}
\StringTok{"title"}\NormalTok{,}
\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)),}
\NormalTok{ (}
\StringTok{"category"}\NormalTok{,}
\NormalTok{ models.ForeignKey(}
\NormalTok{ on\_delete}\OperatorTok{=}\NormalTok{django.db.models.deletion.CASCADE,}
\NormalTok{ to}\OperatorTok{=}\StringTok{"library.category"}\NormalTok{,}
\NormalTok{ ),}
\NormalTok{ ),}
\NormalTok{ ],}
\NormalTok{ ),}
\NormalTok{ ]}
\end{Highlighting}
\end{Shaded}
\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{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Category(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Book(models.Model):}
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ManyManyField(Category) }
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Vous noterez que l'attribut \texttt{on\_delete} n'est plus nécessaire.
\end{itemize}
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. C'est qu'applique
la migration en supprimant le champ liant initialement un livre à une
catégorie et en ajoutant une nouvelle table de liaison.
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# library/migrations/0002\_remove\_book\_category\_book\_category.py}
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ (}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0001\_initial\textquotesingle{}}\NormalTok{),}
\NormalTok{ ]}
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ migrations.RemoveField( }
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
\NormalTok{ ),}
\NormalTok{ migrations.AddField( }
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.ManyToManyField(to}\OperatorTok{=}\StringTok{\textquotesingle{}library.Category\textquotesingle{}}\NormalTok{),}
\NormalTok{ ),}
\NormalTok{ ]}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
La migration supprime l'ancienne clé étrangère\ldots\hspace{0pt}
\item
\ldots\hspace{0pt} 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}
\hypertarget{_graph_de_duxe9pendances}{%
\subsection{Graph de dépendances}\label{_graph_de_duxe9pendances}}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{[snip]}
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
\StringTok{\textquotesingle{}django.contrib.admin\textquotesingle{}}\NormalTok{, }
\StringTok{\textquotesingle{}django.contrib.auth\textquotesingle{}}\NormalTok{, }
\StringTok{\textquotesingle{}django.contrib.contenttypes\textquotesingle{}}\NormalTok{, }
\StringTok{\textquotesingle{}django.contrib.sessions\textquotesingle{}}\NormalTok{, }
\StringTok{\textquotesingle{}django.contrib.messages\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.staticfiles\textquotesingle{}}\NormalTok{,}
\NormalTok{]}
\NormalTok{[snip]}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Admin
\item
Auth
\item
Contenttypes
\item
et Sessions.
\end{itemize}
Dès que nous les appliquerons, nous recevrons les messages suivants:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py migrate}
\ExtensionTok{Operations}\NormalTok{ to perform:}
\ExtensionTok{Apply}\NormalTok{ all migrations: admin, auth, contenttypes, library, sessions, world}
\ExtensionTok{Running}\NormalTok{ migrations:}
\ExtensionTok{Applying}\NormalTok{ contenttypes.0001\_initial... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0001\_initial... OK}
\ExtensionTok{Applying}\NormalTok{ admin.0001\_initial... OK}
\ExtensionTok{Applying}\NormalTok{ admin.0002\_logentry\_remove\_auto\_add... OK}
\ExtensionTok{Applying}\NormalTok{ admin.0003\_logentry\_add\_action\_flag\_choices... OK}
\ExtensionTok{Applying}\NormalTok{ contenttypes.0002\_remove\_content\_type\_name... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0002\_alter\_permission\_name\_max\_length... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0003\_alter\_user\_email\_max\_length... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0004\_alter\_user\_username\_opts... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0005\_alter\_user\_last\_login\_null... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0006\_require\_contenttypes\_0002... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0007\_alter\_validators\_add\_error\_messages... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0008\_alter\_user\_username\_max\_length... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0009\_alter\_user\_last\_name\_max\_length... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0010\_alter\_group\_name\_max\_length... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0011\_update\_proxy\_permissions... OK}
\ExtensionTok{Applying}\NormalTok{ auth.0012\_alter\_user\_first\_name\_max\_length... OK}
\ExtensionTok{Applying}\NormalTok{ sessions.0001\_initial... OK}
\end{Highlighting}
\end{Shaded}
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}
\hypertarget{_sous_le_capot}{%
\subsection{Sous le capot}\label{_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}
\def\labelenumi{\arabic{enumi}.}
\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{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
\ImportTok{import}\NormalTok{ django.db.models.deletion}
\ImportTok{import}\NormalTok{ django.utils.timezone}
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ (}\StringTok{\textquotesingle{}gwift\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0004\_name\_value\textquotesingle{}}\NormalTok{),}
\NormalTok{ ]}
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ migrations.AddField(}
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlist\textquotesingle{}}\NormalTok{,}
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{,}
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.TextField(default}\OperatorTok{=}\StringTok{""}\NormalTok{, null}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
\NormalTok{ preserve\_default}\OperatorTok{=}\VariableTok{False}\NormalTok{,}
\NormalTok{ ),}
\NormalTok{ ]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_liste_des_migrations}{%
\subsection{Liste des migrations}\label{_liste_des_migrations}}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py showmigrations}
\ExtensionTok{admin}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_logentry\_remove\_auto\_add}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0003\_logentry\_add\_action\_flag\_choices}
\ExtensionTok{auth}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_alter\_permission\_name\_max\_length}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0003\_alter\_user\_email\_max\_length}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0004\_alter\_user\_username\_opts}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0005\_alter\_user\_last\_login\_null}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0006\_require\_contenttypes\_0002}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0007\_alter\_validators\_add\_error\_messages}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0008\_alter\_user\_username\_max\_length}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0009\_alter\_user\_last\_name\_max\_length}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0010\_alter\_group\_name\_max\_length}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0011\_update\_proxy\_permissions}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0012\_alter\_user\_first\_name\_max\_length}
\ExtensionTok{contenttypes}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_remove\_content\_type\_name}
\ExtensionTok{library}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_remove\_book\_category\_book\_category}
\BuiltInTok{ [ ]} \ExtensionTok{0003\_book\_summary}
\ExtensionTok{sessions}
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
\end{Highlighting}
\end{Shaded}
\hypertarget{_squash}{%
\subsection{Squash}\label{_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{Shaded}
\begin{Highlighting}[]
\CommentTok{\# library/migrations/0002\_remove\_book\_category.py}
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ (}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0001\_initial\textquotesingle{}}\NormalTok{),}
\NormalTok{ ]}
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ migrations.RemoveField(}
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
\NormalTok{ ),}
\NormalTok{ migrations.AddField(}
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.ManyToManyField(to}\OperatorTok{=}\StringTok{\textquotesingle{}library.Category\textquotesingle{}}\NormalTok{),}
\NormalTok{ ),}
\NormalTok{ ]}
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# library/migrations/0003\_book\_summary.py}
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ (}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0002\_remove\_book\_category\_book\_category\textquotesingle{}}\NormalTok{),}
\NormalTok{ ]}
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ migrations.AddField(}
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}summary\textquotesingle{}}\NormalTok{,}
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.TextField(blank}\OperatorTok{=}\VariableTok{True}\NormalTok{),}
\NormalTok{ ),}
\NormalTok{ ]}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py squashmigrations library 0002 0003}
\ExtensionTok{Will}\NormalTok{ squash the following migrations:}
\ExtensionTok{{-}}\NormalTok{ 0002\_remove\_book\_category\_book\_category}
\ExtensionTok{{-}}\NormalTok{ 0003\_book\_summary}
\ExtensionTok{Do}\NormalTok{ you wish to proceed? [yN] y}
\ExtensionTok{Optimizing...}
\ExtensionTok{No}\NormalTok{ optimizations possible.}
\ExtensionTok{Created}\NormalTok{ new squashed migration /home/fred/Sources/gwlib/library/migrations/0002\_remove\_book\_category\_book\_category\_squashed\_0003\_book\_summary.py}
\ExtensionTok{You}\NormalTok{ should commit this migration but leave the old ones in place}\KeywordTok{;}
\ExtensionTok{the}\NormalTok{ new migration will be used for new installs. Once you are sure}
\ExtensionTok{all}\NormalTok{ instances of the codebase have applied the migrations you squashed,}
\ExtensionTok{you}\NormalTok{ can delete them.}
\end{Highlighting}
\end{Shaded}
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{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\FunctionTok{cat}\NormalTok{ library/migrations/0002\_remove\_book\_category\_book\_category\_squashed\_0003\_book\_summary.py}
\CommentTok{\# Generated by Django 4.0.3 on 2022{-}03{-}15 18:01}
\ExtensionTok{from}\NormalTok{ django.db import migrations, models}
\ExtensionTok{class}\NormalTok{ Migration(migrations.Migration)}\BuiltInTok{:}
\ExtensionTok{replaces}\NormalTok{ = [(}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0002\_remove\_book\_category\_book\_category\textquotesingle{}}\NormalTok{), }\KeywordTok{(}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0003\_book\_summary\textquotesingle{}}\KeywordTok{)}\NormalTok{]}
\ExtensionTok{dependencies}\NormalTok{ = [}
\KeywordTok{(}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0001\_initial\textquotesingle{}}\KeywordTok{)}\NormalTok{,}
\NormalTok{ ]}
\ExtensionTok{operations}\NormalTok{ = [}
\ExtensionTok{migrations.RemoveField}\NormalTok{(}
\VariableTok{model\_name=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\VariableTok{name=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
\NormalTok{ ),}
\ExtensionTok{migrations.AddField}\NormalTok{(}
\VariableTok{model\_name=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\VariableTok{name=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
\VariableTok{field=}\NormalTok{models.ManyToManyField}\VariableTok{(}\NormalTok{to}\VariableTok{=}\StringTok{\textquotesingle{}library.category\textquotesingle{}}\VariableTok{)}\NormalTok{,}
\NormalTok{ ),}
\ExtensionTok{migrations.AddField}\NormalTok{(}
\VariableTok{model\_name=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
\VariableTok{name=}\StringTok{\textquotesingle{}summary\textquotesingle{}}\NormalTok{,}
\VariableTok{field=}\NormalTok{models.TextField}\VariableTok{(}\NormalTok{blank}\VariableTok{=}\NormalTok{True}\VariableTok{)}\NormalTok{,}
\NormalTok{ ),}
\NormalTok{ ]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_ruxe9initialisation_dune_ou_plusieurs_migrations}{%
\subsection{Réinitialisation d'une ou plusieurs
migrations}\label{_ruxe9initialisation_dune_ou_plusieurs_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}
\hypertarget{_shell}{%
\section{Shell}\label{_shell}}
\hypertarget{_administration}{%
\section{Administration}\label{_administration}}
Woké. On va commencer par la \textbf{partie à ne \emph{surtout}
(\emph{surtout} !!) pas faire en premier dans un projet Django}. Mais on
va la faire quand même: la raison principale est que 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. Mais 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. Mais 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}
\hypertarget{_le_moduxe8le_de_donnuxe9es}{%
\subsection{Le modèle de données}\label{_le_moduxe8le_de_donnuxe9es}}
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:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path}
\ImportTok{from}\NormalTok{ gwift.views }\ImportTok{import}\NormalTok{ wish\_details}
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls), }
\NormalTok{ [...]}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Cette URL signifie que la partie \texttt{admin} est déjà active et
accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}
\end{itemize}
C'est le seul prérequis pour cette partie.
Chaque application nouvellement créée contient par défaut un fichier
\texttt{admin.py}, dans lequel il est possible de déclarer quel ensemble
de données sera accessible/éditable. Ainsi, si nous partons du modèle
basique que nous avions détaillé plus tôt, avec des souhaits et des
listes de souhaits:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# gwift/wish/models.py}
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ models}
\KeywordTok{class}\NormalTok{ WishList(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Item(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ models.ForeignKey(WishList, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\end{Highlighting}
\end{Shaded}
Nous pouvons facilement arriver au résultat suivant, en ajoutant
quelques lignes de configuration dans ce fichier \texttt{admin.py}:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ Item, WishList }
\NormalTok{admin.site.register(Item) }
\NormalTok{admin.site.register(WishList)}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Nous importons les modèles que nous souhaitons gérer dans l'admin
\item
Et nous les déclarons comme gérables. Cette dernière ligne implique
aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui
n'activera simplement aucune opération de lecture ou modification.
\end{itemize}
Il nous reste une seule étape à réaliser: créer un nouvel utilisateur.
Pour cet exemple, notre gestion va se limiter à une gestion manuelle;
nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons
créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{python}\NormalTok{ manage.py createsuperuser}
\ExtensionTok{Username}\NormalTok{ (leave blank to use }\StringTok{\textquotesingle{}fred\textquotesingle{}}\NormalTok{)}\BuiltInTok{:}\NormalTok{ fred}
\ExtensionTok{Email}\NormalTok{ address: fred@root.org}
\ExtensionTok{Password}\NormalTok{: ******}
\ExtensionTok{Password}\NormalTok{ (again)}\BuiltInTok{:}\NormalTok{ ******}
\ExtensionTok{Superuser}\NormalTok{ created successfully.}
\end{Highlighting}
\end{Shaded}
\begin{figure}
\centering
\includegraphics{images/django/django-site-admin.png}
\caption{Connexion au site d'administration}
\end{figure}
\begin{figure}
\centering
\includegraphics{images/django/django-site-admin-after-connection.png}
\caption{Administration}
\end{figure}
\hypertarget{_quelques_conseils_de_base}{%
\subsection{Quelques conseils de
base}\label{_quelques_conseils_de_base}}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Surchargez la méthode \texttt{str(self)} pour chaque classe que vous
aurez définie dans le modèle. Cela permettra de construire une
représentation textuelle pour chaque instance de votre classe. Cette
information sera utilisée un peu partout dans le code, et donnera une
meilleure idée de ce que l'on manipule. En plus, cette méthode est
également appelée lorsque l'administration historisera une action (et
comme cette étape sera inaltérable, autant qu'elle soit fixée dans le
début).
\item
La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle
on peut accéder pour obtenir les détails d'une instance. Par exemple:
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ get\_absolute\_url(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return}\NormalTok{ reverse(}\StringTok{\textquotesingle{}myapp.views.details\textquotesingle{}}\NormalTok{, args}\OperatorTok{=}\NormalTok{[}\VariableTok{self}\NormalTok{.}\BuiltInTok{id}\NormalTok{])}
\end{Highlighting}
\end{Shaded}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Les attributs \texttt{Meta}:
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ ordering }\OperatorTok{=}\NormalTok{ [}\StringTok{\textquotesingle{}{-}field1\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}field2\textquotesingle{}}\NormalTok{]}
\NormalTok{ verbose\_name }\OperatorTok{=} \StringTok{\textquotesingle{}my class in singular\textquotesingle{}}
\NormalTok{ verbose\_name\_plural }\OperatorTok{=} \StringTok{\textquotesingle{}my class when is in a list!\textquotesingle{}}
\end{Highlighting}
\end{Shaded}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le titre:
\begin{itemize}
\item
Soit en modifiant le template de l'administration
\item
Soit en ajoutant l'assignation suivante dans le fichier
\texttt{urls.py}:
\texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}.
\end{itemize}
\item
Prefetch
\end{enumerate}
\url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64}
\url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614}
En gros, le problème de l'admin est que si on fait des requêtes
imbriquées, on va flinguer l'application et le chargement de la page. La
solution consiste à utiliser la propriété \texttt{list\_select\_related}
de la classe d'Admin, afin d'appliquer une jointure par défaut et et
gagner en performances.
\hypertarget{_admin_modeladmin}{%
\subsection{admin.ModelAdmin}\label{_admin_modeladmin}}
La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement
dans le fichier \texttt{admin.py} de chaque application contiendra la
définition de ce que l'on souhaite faire avec nos données dans
l'administration. Cette classe (et sa partie Meta)
\hypertarget{_laffichage}{%
\subsection{L'affichage}\label{_laffichage}}
Comme l'interface d'administration fonctionne (en trèèèès) gros comme un
CRUD auto-généré, on trouve par défaut la possibilité de :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Créer de nouveaux éléments
\item
Lister les éléments existants
\item
Modifier des éléments existants
\item
Supprimer un élément en particulier.
\end{enumerate}
Les affichages sont donc de deux types: en liste et par élément.
Pour les affichages en liste, le plus simple consiste à jouer sur la
propriété \texttt{list\_display}.
Par défaut, la première colonne va accueillir le lien vers le formulaire
d'édition. On peut donc modifier ceci, voire créer de nouveaux liens
vers d'autres éléments en construisant des URLs dynamiquement.
(Insérer ici l'exemple de Medplan pour les liens vers les postgradués
:-))
Voir aussi comment personnaliser le fil d'Ariane ?
\hypertarget{_les_filtres}{%
\subsection{Les filtres}\label{_les_filtres}}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
list\_filter
\item
filter\_horizontal
\item
filter\_vertical
\item
date\_hierarchy
\end{enumerate}
\hypertarget{_les_permissions}{%
\subsection{Les permissions}\label{_les_permissions}}
On l'a dit plus haut, il vaut mieux éviter de proposer un accès à
l'administration à vos utilisateurs. Il est cependant possible de
configurer des permissions spécifiques pour certains groupes, en leur
autorisant certaines actions de visualisation/ajout/édition ou
suppression.
Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les
méthodes suivantes:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ has\_add\_permission(}\VariableTok{self}\NormalTok{, request):}
\ControlFlowTok{return} \VariableTok{True}
\KeywordTok{def}\NormalTok{ has\_delete\_permission(}\VariableTok{self}\NormalTok{, request):}
\ControlFlowTok{return} \VariableTok{True}
\KeywordTok{def}\NormalTok{ has\_change\_permission(}\VariableTok{self}\NormalTok{, request):}
\ControlFlowTok{return} \VariableTok{True}
\end{Highlighting}
\end{Shaded}
On peut accéder aux informations de l'utilisateur actuellement connecté
au travers de l'objet \texttt{request.user}.
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
NOTE: ajouter un ou deux screenshots :-)
\end{enumerate}
\hypertarget{_les_relations}{%
\subsection{Les relations}\label{_les_relations}}
\hypertarget{_les_relations_1_n}{%
\subsubsection{Les relations 1-n}\label{_les_relations_1_n}}
Les relations 1-n sont implémentées au travers de formsets (que l'on a
normalement déjà décrits plus haut). L'administration permet de les
définir d'une manière extrêmement simple, grâce à quelques propriétés.
L'implémentation consiste tout d'abord à définir le comportement du type
d'objet référencé (la relation -N), puis à inclure cette définition au
niveau du type d'objet référençant (la relation 1-).
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ WishInline(TabularInline):}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Wish}
\KeywordTok{class}\NormalTok{ Wishlist(admin.ModelAdmin):}
\NormalTok{ ...}
\NormalTok{ inlines }\OperatorTok{=}\NormalTok{ [WishInline]}
\NormalTok{ ...}
\end{Highlighting}
\end{Shaded}
Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist})
pourra directement gérer des relations multiples vers des souhaits.
\hypertarget{_les_auto_suggestions_et_auto_compluxe9tions}{%
\subsubsection{Les auto-suggestions et
auto-complétions}\label{_les_auto_suggestions_et_auto_compluxe9tions}}
Parler de l'intégration de select2.
\hypertarget{_la_pruxe9sentation}{%
\subsection{La présentation}\label{_la_pruxe9sentation}}
Parler ici des \texttt{fieldsets} et montrer comment on peut regrouper
des champs dans des groupes, ajouter un peu de javascript,
\ldots\hspace{0pt}
\hypertarget{_les_actions_sur_des_suxe9lections}{%
\subsection{Les actions sur des
sélections}\label{_les_actions_sur_des_suxe9lections}}
Les actions permettent de partir d'une liste d'éléments, et autorisent
un utilisateur à appliquer une action sur une sélection d'éléments. Par
défaut, il existe déjà une action de \textbf{suppression}.
Les paramètres d'entrée sont :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
L'instance de classe
\item
La requête entrante
\item
Le queryset correspondant à la sélection.
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{def}\NormalTok{ double\_quantity(}\VariableTok{self}\NormalTok{, request, queryset):}
\ControlFlowTok{for}\NormalTok{ obj }\KeywordTok{in}\NormalTok{ queryset.}\BuiltInTok{all}\NormalTok{():}
\NormalTok{ obj.field }\OperatorTok{+=} \DecValTok{1}
\NormalTok{ obj.save()}
\NormalTok{double\_quantity.short\_description }\OperatorTok{=} \StringTok{"Doubler la quantité des souhaits."}
\end{Highlighting}
\end{Shaded}
Et pour informer l'utilisateur de ce qui a été réalisé, on peut aussi
lui passer un petit message:
\begin{Shaded}
\begin{Highlighting}[]
\ControlFlowTok{if}\NormalTok{ rows\_updated }\OperatorTok{=} \DecValTok{0}\NormalTok{:}
\VariableTok{self}\NormalTok{.message\_user(request, }\StringTok{"Aucun élément n\textquotesingle{}a été impacté."}\NormalTok{)}
\ControlFlowTok{else}\NormalTok{:}
\VariableTok{self}\NormalTok{.message\_user(request, }\StringTok{"}\SpecialCharTok{\{\}}\StringTok{ élément(s) mis à jour"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(rows\_updated))}
\end{Highlighting}
\end{Shaded}
\hypertarget{_la_documentation}{%
\subsection{La documentation}\label{_la_documentation}}
Nous l'avons dit plus haut, l'administration de Django a également la
possibilité de rendre accessible la documentation associée à un modèle
de données. Pour cela, il suffit de suivre les bonnes pratiques, puis
\href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer
la documentation à partir des URLs}:
\begin{Shaded}
\begin{Highlighting}[]
\end{Highlighting}
\end{Shaded}
\hypertarget{_forms}{%
\section{Forms}\label{_forms}}
\begin{quote}
Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et
que l'ensemble reste cohérent. Il (le form) n'a pas à savoir que tu as
implémenté des closure tables dans un graph dirigé acyclique.
\end{quote}
Ou comment valider proprement des données entrantes.
\includegraphics{images/xkcd-327.png}
Quand on parle de \texttt{forms}, on ne parle pas uniquement de
formulaires Web. On pourrait considérer qu'il s'agit de leur objectif
principal, mais on peut également voir un peu plus loin: on peut en fait
voir les \texttt{forms} comme le point d'entrée pour chaque donnée
arrivant dans notre application: il s'agit en quelque sorte d'un
ensemble de règles complémentaires à celles déjà présentes au niveau du
modèle.
L'exemple le plus simple est un fichier \texttt{.csv}: la lecture de ce
fichier pourrait se faire de manière très simple, en récupérant les
valeurs de chaque colonne et en l'introduisant dans une instance du
modèle.
Mauvaise idée. On peut proposer trois versions d'un même code, de la
version simple (lecture du fichier csv et jonglage avec les indices de
colonnes), puis une version plus sophistiquée (et plus lisible, à base
de
\href{https://docs.python.org/3/library/csv.html\#csv.DictReader}{DictReader}),
et la version + à base de form.
Les données fournies par un utilisateur \textbf{doivent}
\textbf{toujours} être validées avant introduction dans la base de
données. Notre base de données étant accessible ici par l'ORM, la
solution consiste à introduire une couche supplémentaire de validation.
Le flux à suivre est le suivant:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Création d'une instance grâce à un dictionnaire
\item
Validation des données et des informations reçues
\item
Traitement, si la validation a réussi.
\end{enumerate}
Ils jouent également deux rôles importants:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Valider des données, en plus de celles déjà définies au niveau du
modèle
\item
Contrôler le rendu à appliquer aux champs.
\end{enumerate}
Ils agissent come une glue entre l'utilisateur et la modélisation de vos
structures de données.
\hypertarget{_flux_de_validation}{%
\subsection{Flux de validation}\label{_flux_de_validation}}
\textbar{} .Validation \textbar{} .is\_valid \textbar{} .clean\_fields ↓
.clean\_fields\_machin
A compléter ;-)
\hypertarget{_duxe9pendance_avec_le_moduxe8le}{%
\subsection{Dépendance avec le
modèle}\label{_duxe9pendance_avec_le_moduxe8le}}
Un \textbf{form} peut dépendre d'une autre classe Django. Pour cela, il
suffit de fixer l'attribut \texttt{model} au niveau de la
\texttt{class\ Meta} dans la définition.
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django }\ImportTok{import}\NormalTok{ forms}
\ImportTok{from}\NormalTok{ wish.models }\ImportTok{import}\NormalTok{ Wishlist}
\KeywordTok{class}\NormalTok{ WishlistCreateForm(forms.ModelForm):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Wishlist}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}name\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
De cette manière, notre form dépendra automatiquement des champs déjà
déclarés dans la classe \texttt{Wishlist}. Cela suit le principe de
\texttt{DRY\ \textless{}dont\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ quune\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ lattribut\ \textasciigrave{}fields},
nous pourrons nous assurer de faire évoluer le formulaire en fonction du
modèle sur lequel il se base.
\hypertarget{_rendu_et_affichage}{%
\subsection{Rendu et affichage}\label{_rendu_et_affichage}}
Le formulaire permet également de contrôler le rendu qui sera appliqué
lors de la génération de la page. Si les champs dépendent du modèle sur
lequel se base le formulaire, ces widgets doivent être initialisés dans
l'attribut \texttt{Meta}. Sinon, ils peuvent l'être directement au
niveau du champ.
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ date}
\ImportTok{from}\NormalTok{ django }\ImportTok{import}\NormalTok{ forms}
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ Accident}
\KeywordTok{class}\NormalTok{ AccidentForm(forms.ModelForm):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Accident}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}gymnast\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}educative\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}date\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}information\textquotesingle{}}\NormalTok{)}
\NormalTok{ widgets }\OperatorTok{=}\NormalTok{ \{}
\StringTok{\textquotesingle{}date\textquotesingle{}}\NormalTok{ : forms.TextInput(}
\NormalTok{ attrs}\OperatorTok{=}\NormalTok{\{}
\StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}form{-}control\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}data{-}provide\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}datepicker\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}data{-}date{-}format\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}dd/mm/yyyy\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}placeholder\textquotesingle{}}\NormalTok{ : date.today().strftime(}\StringTok{"}\SpecialCharTok{\%d}\StringTok{/\%m/\%Y"}\NormalTok{)}
\NormalTok{ \}),}
\StringTok{\textquotesingle{}information\textquotesingle{}}\NormalTok{ : forms.Textarea(}
\NormalTok{ attrs}\OperatorTok{=}\NormalTok{\{}
\StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}form{-}control\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}placeholder\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}Context (why, where, ...)\textquotesingle{}}
\NormalTok{ \})}
\NormalTok{ \}}
\end{Highlighting}
\end{Shaded}
\hypertarget{_squelette_par_duxe9faut}{%
\subsection{Squelette par défaut}\label{_squelette_par_duxe9faut}}
On a d'un côté le \{\{ form.as\_p \}\} ou \{\{ form.as\_table \}\}, mais
il y a beaucoup mieux que ça ;-) Voir les templates de Vitor et en
passant par \texttt{widget-tweaks}.
\hypertarget{_crispy_forms}{%
\subsection{Crispy-forms}\label{_crispy_forms}}
Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela
permet de valider des données reçues en entrée et d'afficher (très)
facilement des formulaires à compléter par l'utilisateur.
Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu
l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir,
modifier les types de contrôleurs, les placer au pixel près,
\ldots\hspace{0pt} Tout ça demande énormément de temps. Et c'est là
qu'intervient
\href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}.
Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation
et uni-form) et permet de contrôler entièrement le \textbf{layout} et la
présentation.
(c/c depuis le lien ci-dessous)
Pour chaque champ, crispy-forms va :
\begin{itemize}
\item
utiliser le \texttt{verbose\_name} comme label.
\item
vérifier les paramètres \texttt{blank} et \texttt{null} pour savoir si
le champ est obligatoire.
\item
utiliser le type de champ pour définir le type de la balise
\texttt{\textless{}input\textgreater{}}.
\item
récupérer les valeurs du paramètre \texttt{choices} (si présent) pour
la balise \texttt{\textless{}select\textgreater{}}.
\end{itemize}
\url{http://dotmobo.github.io/django-crispy-forms.html}
\hypertarget{_en_conclusion}{%
\subsection{En conclusion}\label{_en_conclusion}}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Toute donnée entrée par l'utilisateur \textbf{doit} passer par une
instance de \texttt{form}.
\item
euh ?
\end{enumerate}
\hypertarget{_authentification}{%
\section{Authentification}\label{_authentification}}
Comme on l'a vu dans la partie sur le modèle, nous souhaitons que le
créateur d'une liste puisse retrouver facilement les éléments qu'il aura
créé. Ce dont nous n'avons pas parlé cependant, c'est la manière dont
l'utilisateur va pouvoir créer son compte et s'authentifier. La
\href{https://docs.djangoproject.com/en/stable/topics/auth/}{documentation}
est très complète, nous allons essayer de la simplifier au maximum.
Accrochez-vous, le sujet peut être complexe.
\hypertarget{_muxe9canisme_dauthentification}{%
\subsection{Mécanisme
d'authentification}\label{_muxe9canisme_dauthentification}}
On peut schématiser le flux d'authentification de la manière suivante :
En gros:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
La personne accède à une URL qui est protégée (voir les décorateurs
@login\_required et le mixin LoginRequiredMixin)
\item
Le framework détecte qu'il est nécessaire pour la personne de se
connecter (grâce à un paramètre type LOGIN\_URL)
\item
Le framework présente une page de connexion ou un mécanisme d'accès
pour la personne (template à définir)
\item
Le framework récupère les informations du formulaire, et les transmets
aux différents backends d'authentification, dans l'ordre
\item
Chaque backend va appliquer la méthode \texttt{authenticate} en
cascade, jusqu'à ce qu'un backend réponde True ou qu'aucun ne réponde
\item
La réponse de la méthode authenticate doit être une instance d'un
utilisateur, tel que définit parmi les paramètres généraux de
l'application.
\end{enumerate}
En résumé (bis):
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Une personne souhaite se connecter;
\item
Les backends d'authentification s'enchaîne jusqu'à trouver une bonne
correspondance. Si aucune correspondance n'est trouvée, on envoie la
personne sur les roses.
\item
Si OK, on retourne une instance de type current\_user, qui pourra être
utilisée de manière uniforme dans l'application.
\end{enumerate}
Ci-dessous, on définit deux backends différents pour mieux comprendre
les différentes possibilités:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Une authentification par jeton
\item
Une authentification LDAP
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ datetime}
\ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model}
\ImportTok{from}\NormalTok{ django.db.models }\ImportTok{import}\NormalTok{ Q}
\ImportTok{from}\NormalTok{ accounts.models }\ImportTok{import}\NormalTok{ Token }
\NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()}
\KeywordTok{class}\NormalTok{ TokenBackend(backends.ModelBackend):}
\KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):}
\CommentTok{"""Authentifie l\textquotesingle{}utilisateur sur base d\textquotesingle{}un jeton qu\textquotesingle{}il a reçu.}
\CommentTok{ On regarde la date de validité de chaque jeton avant d\textquotesingle{}autoriser l\textquotesingle{}accès.}
\CommentTok{ """}
\NormalTok{ token }\OperatorTok{=}\NormalTok{ kwargs.get(}\StringTok{"token"}\NormalTok{, }\VariableTok{None}\NormalTok{)}
\NormalTok{ current\_token }\OperatorTok{=}\NormalTok{ Token.objects.}\BuiltInTok{filter}\NormalTok{(token}\OperatorTok{=}\NormalTok{token, validity\_date\_\_gte}\OperatorTok{=}\NormalTok{datetime.now()).first()}
\ControlFlowTok{if}\NormalTok{ current\_token:}
\NormalTok{ user }\OperatorTok{=}\NormalTok{ current\_token.user}
\NormalTok{ current\_token.last\_used\_date }\OperatorTok{=}\NormalTok{ datetime.now()}
\NormalTok{ current\_token.save()}
\ControlFlowTok{return}\NormalTok{ user}
\ControlFlowTok{return} \VariableTok{None}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Sous-entend qu'on a bien une classe qui permet d'accéder à ces jetons
;-)
\end{itemize}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model}
\ImportTok{from}\NormalTok{ ldap3 }\ImportTok{import}\NormalTok{ Server, Connection, ALL}
\ImportTok{from}\NormalTok{ ldap3.core.exceptions }\ImportTok{import}\NormalTok{ LDAPPasswordIsMandatoryError}
\ImportTok{from}\NormalTok{ config }\ImportTok{import}\NormalTok{ settings}
\NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()}
\KeywordTok{class}\NormalTok{ LdapBackend(backends.ModelBackend):}
\CommentTok{"""Implémentation du backend LDAP pour la connexion des utilisateurs à l\textquotesingle{}Active Directory.}
\CommentTok{ """}
\KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):}
\CommentTok{"""Authentifie l\textquotesingle{}utilisateur au travers du serveur LDAP.}
\CommentTok{ """}
\NormalTok{ ldap\_server }\OperatorTok{=}\NormalTok{ Server(settings.LDAP\_SERVER, get\_info}\OperatorTok{=}\NormalTok{ALL)}
\NormalTok{ ldap\_connection }\OperatorTok{=}\NormalTok{ Connection(ldap\_server, user}\OperatorTok{=}\NormalTok{username, password}\OperatorTok{=}\NormalTok{password)}
\ControlFlowTok{try}\NormalTok{:}
\ControlFlowTok{if} \KeywordTok{not}\NormalTok{ ldap\_connection.bind():}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Login ou mot de passe incorrect"}\NormalTok{)}
\ControlFlowTok{except}\NormalTok{ (LDAPPasswordIsMandatoryError, }\PreprocessorTok{ValueError}\NormalTok{) }\ImportTok{as}\NormalTok{ ldap\_exception:}
\ControlFlowTok{raise}\NormalTok{ ldap\_exception}
\NormalTok{ user, \_ }\OperatorTok{=}\NormalTok{ UserModel.objects.get\_or\_create(username}\OperatorTok{=}\NormalTok{username)}
\end{Highlighting}
\end{Shaded}
On peut résumer le mécanisme d'authentification de la manière suivante:
\begin{itemize}
\item
Si vous voulez modifier les informations liées à un utilisateur,
orientez-vous vers la modification du modèle. Comme nous le verrons
ci-dessous, il existe trois manières de prendre ces modifications en
compte. Voir également
\href{https://docs.djangoproject.com/en/stable/topics/auth/customizing/}{ici}.
\item
Si vous souhaitez modifier la manière dont l'utilisateur se connecte,
alors vous devrez modifier le \textbf{backend}.
\end{itemize}
\hypertarget{_modification_du_moduxe8le}{%
\subsection{Modification du modèle}\label{_modification_du_moduxe8le}}
Dans un premier temps, Django a besoin de manipuler
\href{https://docs.djangoproject.com/en/1.9/ref/contrib/auth/\#user-model}{des
instances de type \texttt{django.contrib.auth.User}}. Cette classe
implémente les champs suivants:
\begin{itemize}
\item
\texttt{username}
\item
\texttt{first\_name}
\item
\texttt{last\_name}
\item
\texttt{email}
\item
\texttt{password}
\item
\texttt{date\_joined}.
\end{itemize}
D'autres champs, comme les groupes auxquels l'utilisateur est associé,
ses permissions, savoir s'il est un super-utilisateur,
\ldots\hspace{0pt} sont moins pertinents pour le moment. Avec les
quelques champs déjà définis ci-dessus, nous avons de quoi identifier
correctement nos utilisateurs. Inutile d'implémenter nos propres
classes, puisqu'elles existent déjà :-)
Si vous souhaitez ajouter un champ, il existe trois manières de faire.
\hypertarget{_extension_du_moduxe8le_existant}{%
\subsection{Extension du modèle
existant}\label{_extension_du_moduxe8le_existant}}
Le plus simple consiste à créer une nouvelle classe, et à faire un lien
de type \texttt{OneToOne} vers la classe
\texttt{django.contrib.auth.User}. De cette manière, on ne modifie rien
à la manière dont Django authentife ses utlisateurs: tout ce qu'on fait,
c'est un lien vers une table nouvellement créée, comme on l'a déjà vu au
point {[}\ldots\hspace{0pt}voir l'héritage de modèle{]}. L'avantage de
cette méthode, c'est qu'elle est extrêmement flexible, et qu'on garde
les mécanismes Django standard. Le désavantage, c'est que pour avoir
toutes les informations de notre utilisateur, on sera obligé d'effectuer
une jointure sur le base de données, ce qui pourrait avoir des
conséquences sur les performances.
\hypertarget{_substitution}{%
\subsection{Substitution}\label{_substitution}}
Avant de commencer, sachez que cette étape doit être effectuée
\textbf{avant la première migration}. Le plus simple sera de définir une
nouvelle classe héritant de \texttt{django.contrib.auth.User} et de
spécifier la classe à utiliser dans votre fichier de paramètres. Si ce
paramètre est modifié après que la première migration ait été effectuée,
il ne sera pas pris en compte. Tenez-en compte au moment de modéliser
votre application.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{AUTH\_USER\_MODEL }\OperatorTok{=} \StringTok{\textquotesingle{}myapp.MyUser\textquotesingle{}}
\end{Highlighting}
\end{Shaded}
Notez bien qu'il ne faut pas spécifier le package \texttt{.models} dans
cette injection de dépendances: le schéma à indiquer est bien
\texttt{\textless{}nom\ de\ lapplication\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}.
\hypertarget{_backend}{%
\subsubsection{Backend}\label{_backend}}
\hypertarget{_templates}{%
\subsubsection{Templates}\label{_templates}}
Ce qui n'existe pas par contre, ce sont les vues. Django propose donc
tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors
administration). En premier lieu, ces paramètres sont fixés dans le
fichier `settings
\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}.
On y trouve par exemple les paramètres suivants:
\begin{itemize}
\item
\texttt{LOGIN\_REDIRECT\_URL}: si vous ne spécifiez pas le paramètre
\texttt{next}, l'utilisateur sera automatiquement redirigé vers cette
page.
\item
\texttt{LOGIN\_URL}: l'URL de connexion à utiliser. Par défaut,
l'utilisateur doit se rendre sur la page \texttt{/accounts/login}.
\end{itemize}
\hypertarget{_social_authentification}{%
\subsubsection{Social-Authentification}\label{_social_authentification}}
Voir ici : \href{https://github.com/omab/python-social-auth}{python
social auth}
\hypertarget{_un_petit_mot_sur_oauth}{%
\subsubsection{Un petit mot sur OAuth}\label{_un_petit_mot_sur_oauth}}
OAuth est un standard libre définissant un ensemble de méthodes à
implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement
se base sur un système de jetons (Tokens), attribués par le possesseur
de la ressource à laquelle un utilisateur souhaite accéder.
Le client initie la connexion en demandant un jeton au serveur. Ce jeton
est ensuite utilisée tout au long de la connexion, pour accéder aux
différentes ressources offertes par ce serveur. `wikipedia
\textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}.
Une introduction à OAuth est
\href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}. Elle
introduit le protocole comme étant une \texttt{valet\ key}, une clé que
l'on donne à la personne qui va garer votre voiture pendant que vous
profitez des mondanités. Cette clé donne un accès à votre voiture, tout
en bloquant un ensemble de fonctionnalités. Le principe du protocole est
semblable en ce sens: vous vous réservez un accès total à une API,
tandis que le système de jetons permet d'identifier une personne, tout
en lui donnant un accès restreint à votre application.
L'utilisation de jetons permet notamment de définir une durée
d'utilisation et une portée d'utilisation. L'utilisateur d'un service A
peut par exemple autoriser un service B à accéder à des ressources qu'il
possède, sans pour autant révéler son nom d'utilisateur ou son mot de
passe.
L'exemple repris au niveau du
\href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le
suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site
faji.com (A). Elle souhaite les imprimer au travers du site beppa.com
(B). Au moment de la commande, le site beppa.com envoie une demande au
site faji.com pour accéder aux ressources partagées par Jane. Pour cela,
une nouvelle page s'ouvre pour l'utilisateur, et lui demande
d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de
B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui
permet d'y accéder.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
\StringTok{"django.contrib..."}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
peut être splitté en plusieurs parties:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
\NormalTok{]}
\NormalTok{THIRD\_PARTIES }\OperatorTok{=}\NormalTok{ [}
\NormalTok{]}
\NormalTok{MY\_APPS }\OperatorTok{=}\NormalTok{ [}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_context_processors}{%
\section{\texorpdfstring{\emph{Context
Processors}}{Context Processors}}\label{_context_processors}}
Mise en pratique: un \emph{context processor} sert \emph{grosso-modo} à
peupler l'ensemble des données transmises des vues aux templates avec
des données communes. Un context processor est un peu l'équivalent d'un
middleware, mais entre les données et les templates, là où le middleware
va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# core/context\_processors.py}
\ImportTok{import}\NormalTok{ subprocess}
\KeywordTok{def}\NormalTok{ git\_describe(request) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{str}\NormalTok{:}
\ControlFlowTok{return}\NormalTok{ \{}
\StringTok{"git\_describe"}\NormalTok{: subprocess.check\_output(}
\NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"describe"}\NormalTok{, }\StringTok{"{-}{-}always"}\NormalTok{]}
\NormalTok{ ).strip(),}
\StringTok{"git\_date"}\NormalTok{: subprocess.check\_output(}
\NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"show"}\NormalTok{, }\StringTok{"{-}s"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}format=}\SpecialCharTok{\%c}\VerbatimStringTok{d"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}date=format:}\SpecialCharTok{\%d}\VerbatimStringTok{{-}\%m{-}\%Y"}\NormalTok{]}
\NormalTok{ ),}
\NormalTok{ \}}
\end{Highlighting}
\end{Shaded}
Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe}
et \texttt{git\_date} dans tous les contextes de tous les templates de
l'application.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{TEMPLATES }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ \{}
\StringTok{\textquotesingle{}BACKEND\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}django.template.backends.django.DjangoTemplates\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}DIRS\textquotesingle{}}\NormalTok{: [os.path.join(BASE\_DIR, }\StringTok{"templates"}\NormalTok{),],}
\StringTok{\textquotesingle{}APP\_DIRS\textquotesingle{}}\NormalTok{: }\VariableTok{True}\NormalTok{,}
\StringTok{\textquotesingle{}OPTIONS\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}context\_processors\textquotesingle{}}\NormalTok{: [}
\StringTok{\textquotesingle{}django.template.context\_processors.debug\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.template.context\_processors.request\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.auth.context\_processors.auth\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.messages.context\_processors.messages\textquotesingle{}}\NormalTok{,}
\StringTok{"core.context\_processors.git\_describe"}
\NormalTok{ ],}
\NormalTok{ \},}
\NormalTok{ \},}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_tests}{%
\subsection{Tests}\label{_tests}}
\begin{quote}
Tests are part of the system.
--- Robert C. Martin Clean Architecture
\end{quote}
\hypertarget{_types_de_tests}{%
\subsubsection{Types de tests}\label{_types_de_tests}}
Les \textbf{tests unitaires} ciblent typiquement une seule fonction,
classe ou méthode, de manière isolée, en fournissant au développeur
l'assurance que son code réalise ce qu'il en attend. Pour plusieurs
raisons (et notamment en raison de performances), les tests unitaires
utilisent souvent des données stubbées - pour éviter d'appeler le "vrai"
service.
\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 \textbf{tests d'acceptance} vérifient que l'application fonctionne
comme convenu, mais à un plus haut niveau (fonctionnement correct d'une
API, validation d'une chaîne d'actions effectuées par un humain,
\ldots\hspace{0pt}).
\begin{quote}
The objective of acceptance tests is to prove that our application does
what the customer meant it to.
\end{quote}
Les \textbf{tests d'intégration} vérifient que l'application coopère
correctement avec les systèmes périphériques.
De manière plus générale, si nous nous rendons compte que les tests sont
trop compliqués à écrire ou nous coûtent trop de temps, c'est sans doute
que l'architecture de la solution n'est 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\_architecture{]}
\begin{quote}
Martin Fowler observes that, in general, "a ten minute build {[}and test
process{]} is perfectly within reason\ldots\hspace{0pt} {[}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, won't 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}
Au final, le plus important est de toujours corréler les phases de tests
indépendantes du reste du travail (de développement, ici), en
l'automatisant au plus près de sa source de création.
En résumé, il est recommandé de:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\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}
TODO: Voir comment configurer une \texttt{memoryDB} pour l'exécution des
tests.
\hypertarget{_tests_de_nommage}{%
\subsubsection{Tests de nommage}\label{_tests_de_nommage}}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse}
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
\KeywordTok{class}\NormalTok{ HomeTests(TestCase):}
\KeywordTok{def}\NormalTok{ test\_home\_view\_status\_code(}\VariableTok{self}\NormalTok{):}
\NormalTok{ url }\OperatorTok{=}\NormalTok{ reverse(}\StringTok{"home"}\NormalTok{)}
\NormalTok{ response }\OperatorTok{=} \VariableTok{self}\NormalTok{.client.get(url)}
\VariableTok{self}\NormalTok{.assertEquals(response.status\_code, }\DecValTok{200}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
\hypertarget{_tests_durls}{%
\subsubsection{Tests d'urls}\label{_tests_durls}}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse}
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
\ImportTok{from}\NormalTok{ .views }\ImportTok{import}\NormalTok{ home}
\KeywordTok{class}\NormalTok{ HomeTests(TestCase):}
\KeywordTok{def}\NormalTok{ test\_home\_view\_status\_code(}\VariableTok{self}\NormalTok{):}
\NormalTok{ view }\OperatorTok{=}\NormalTok{ resolve(}\StringTok{"/"}\NormalTok{)}
\VariableTok{self}\NormalTok{.assertEquals(view.func, home)}
\end{Highlighting}
\end{Shaded}
\hypertarget{_conclusions_2}{%
\section{Conclusions}\label{_conclusions_2}}
\begin{quote}
To be effective, a software system must be deployable. The higher the
cost of deployements, the less useful the system is. A goal of a
software architecture, then, should be to make a system that can be
easily deployed with a single action. Unfortunately, deployment strategy
is seldom considered during initial development. This leads to
architectures that may be make the system easy to develop, but leave it
very difficult to deploy.
--- Robert C. Martin Clean Architecture
\end{quote}
Il y a une raison très simple à aborder le déploiement dès maintenant: à
trop attendre et à peaufiner son développement en local, on en oublie
que sa finalité sera de se retrouver exposé et accessible depuis un
serveur. Il est du coup probable d'oublier une partie des désidérata, de
zapper une fonctionnalité essentielle ou simplement de passer énormément
de temps à adapter les sources pour qu'elles puissent être mises à
disposition sur un environnement en particulier, une fois que leur
développement aura été finalisé, testé et validé. Un bon déploiement ne
doit pas dépendre de dizaines de petits scripts éparpillés sur le
disque. L'objectif est qu'il soit rapide et fiable. Ceci peut être
atteint au travers d'un partitionnement correct, incluant le fait que le
composant principal s'assure que chaque sous-composant est correctement
démarré intégré et supervisé.
Aborder le déploiement dès le début permet également de rédiger dès le
début les procédures d'installation, de mises à jour et de sauvegardes.
A la fin de chaque intervalle de développement, les fonctionnalités
auront dû avoir été intégrées, testées, fonctionnelles et un code
propre, démontrable dans un environnement similaire à un environnement
de production, et créées à partir d'un tronc commun au développement
cite:{[}devops\_handbook{]}.
Déploier une nouvelle version sera aussi simple que de récupérer la
dernière archive depuis le dépôt, la placer dans le bon répertoire,
appliquer des actions spécifiques (et souvent identiques entre deux
versions), puis redémarrer les services adéquats, et la procédure
complète se résumera à quelques lignes d'un script bash.
\begin{quote}
Because value is created only when our services are running into
production, we must ensure that we are not only delivering fast flow,
but that our deployments can also be performed without causing chaos and
disruptions such as service outages, service impairments, or security or
compliance failures.
--- DevOps Handbook Introduction
\end{quote}
Le serveur que django met à notre disposition \emph{via} la commande
\texttt{runserver} est extrêmement pratique, mais il est uniquement
prévu pour la phase développement: en production, il est inutile de
passer par du code Python pour charger des fichiers statiques (feuilles
de style, fichiers JavaScript, images, \ldots\hspace{0pt}). 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. En production, 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.
L'objectif de cette partie est de parcourir les différentes possibilités
qui s'offrent à nous en termes de déploiement, tout en faisant en sorte
que le code soit le moins couplé possible à sa destination de
production. L'objectif est donc de faire en sorte qu'une même
application puisse être hébergées par plusieurs hôtes sans avoir à subir
de modifications. Nous vous renvoyons vers les 12-facteurs dont nous
avons déjà parlé et qui vous énormément nous aider, puisque ce sont des
variables d'environnement qui vont réellement piloter le câblage entre
l'application, ses composants et son hébergeur.
RedHat proposait récemment un article intitulé \emph{*What Is IaaS*},
qui présentait les principales différences entre types d'hébergement.
\begin{figure}
\centering
\includegraphics{images/deployment/iaas_focus-paas-saas-diagram.png}
\caption{L'infrastructure en tant que service, cc. \emph{RedHat Cloud
Computing}}
\end{figure}
Ainsi, on trouve:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le déploiment \emph{on-premises} ou \emph{on-site}
\item
Les \emph{Infrastructures as a service} ou \emph{IaaSIaaS}
\item
Les \emph{Platforms as a service} ou \emph{PaaSPaaS}
\item
Les \emph{Softwares as a service} ou \emph{SaaSSaaS}, ce dernier point
nous concernant moins, puisque c'est nous qui développons le logiciel.
\end{enumerate}
Dans cette partie, nous aborderons les points suivants:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Définir l'infrastructure et les composants nécessaires à notre
application
\item
Configurer l'hôte qui hébergera l'application et y déployer notre
application: dans une machine physique, virtuelle ou dans un
container. Nous aborderons aussi les déploiements via Ansible et Salt.
A ce stade, nous aurons déjà une application disponible.
\item
Configurer les outils nécessaires à la bonne exécution de ce code et
de ses fonctionnalités: les différentes méthodes de supervision de
l'application, comment analyser les fichiers de logs, comment
intercepter correctement une erreur si elle se présente et comment
remonter correctement l'information.
\end{enumerate}
\hypertarget{_infrastructure_composants}{%
\section{Infrastructure \&
composants}\label{_infrastructure_composants}}
Pour une mise ne production, le standard \emph{de facto} est le suivant:
\begin{itemize}
\item
Nginx comme reverse proxy
\item
HAProxy pour la distribution de charge
\item
Gunicorn ou Uvicorn comme serveur d'application
\item
Supervisor pour le monitoring
\item
PostgreSQL ou MySQL/MariaDB comme bases de données.
\item
Celery et RabbitMQ pour l'exécution de tâches asynchrones
\item
Redis / Memcache pour la mise à en cache (et pour les sessions ? A
vérifier).
\item
Sentry, pour le suivi des bugs
\end{itemize}
Si nous schématisons l'infrastructure et le chemin parcouru par une
requête, nous pourrions arriver à la synthèse suivante:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
L'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
\item
Le navigateur envoie une requête http, sa version, un verbe (GET,
POST, \ldots\hspace{0pt}), un port et éventuellement du contenu
\item
Le firewall du serveur (Debian GNU/Linux, CentOS, \ldots\hspace{0pt})
vérifie si la requête peut être prise en compte
\item
La requête est transmise à l'application qui écoute sur le port
(probablement 80 ou 443; et \emph{a priori} Nginx)
\item
Elle est ensuite transmise par socket et est prise en compte par un
des \emph{workers} (= un processus Python) instancié par Gunicorn. Si
l'un de ces travailleurs venait à planter, il serait automatiquement
réinstancié par Supervisord.
\item
Qui la transmet ensuite à l'un de ses \emph{workers} (= un processus
Python).
\item
Après exécution, une réponse est renvoyée à l'utilisateur.
\end{enumerate}
\includegraphics{images/diagrams/architecture.png}
\hypertarget{_reverse_proxy}{%
\subsection{Reverse proxy}\label{_reverse_proxy}}
Le principe du \textbf{proxy inverse} est de pouvoir rediriger du trafic
entrant vers une application hébergée sur le système. Il serait tout à
fait possible de rendre notre application directement accessible depuis
l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la
sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un
mécanisme de cache ou en compressant certains résultats \footnote{\url{https://fr.wikipedia.org/wiki/Proxy_inverse}}
\hypertarget{_load_balancer}{%
\subsection{Load balancer}\label{_load_balancer}}
\hypertarget{_workers}{%
\subsection{Workers}\label{_workers}}
\hypertarget{_supervision_des_processus}{%
\subsection{Supervision des
processus}\label{_supervision_des_processus}}
\hypertarget{_base_de_donnuxe9es_2}{%
\subsection{Base de données}\label{_base_de_donnuxe9es_2}}
\hypertarget{_tuxe2ches_asynchrones}{%
\subsection{Tâches asynchrones}\label{_tuxe2ches_asynchrones}}
\hypertarget{_mise_en_cache}{%
\subsection{Mise en cache}\label{_mise_en_cache}}
\hypertarget{_code_source}{%
\section{Code source}\label{_code_source}}
Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête
arrive dans les mains du processus Python, qui doit encore
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
effectuer le routage des données,
\item
trouver la bonne fonction à exécuter,
\item
récupérer les données depuis la base de données,
\item
effectuer le rendu ou la conversion des données,
\item
et renvoyer une réponse à l'utilisateur.
\end{enumerate}
Comme nous l'avons vu dans la première partie, Django est un framework
complet, intégrant tous les mécanismes nécessaires à la bonne évolution
d'une application. Il est possible de démarrer petit, et de suivre
l'évolution des besoins en fonction de la charge estimée ou ressentie,
d'ajouter un mécanisme de mise en cache, des logiciels de suivi,
\ldots\hspace{0pt}
\hypertarget{_outils_de_supervision_et_de_mise_uxe0_disposition}{%
\section{Outils de supervision et de mise à
disposition}\label{_outils_de_supervision_et_de_mise_uxe0_disposition}}
\hypertarget{_logs}{%
\subsection{Logs}\label{_logs}}
\hypertarget{_logging}{%
\section{Logging}\label{_logging}}
La structure des niveaux de journaux est essentielle.
\begin{quote}
When deciding whether a message should be ERROR or WARN, imagine being
woken up at 4 a.m. Low printer toner is not an ERROR.
--- Dan North former ToughtWorks consultant
\end{quote}
\begin{itemize}
\item
\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, \ldots\hspace{0pt}
\item
\textbf{WARN}: Regroupe les informations qui pourraient
potentiellement devenir des erreurs.
\item
\textbf{ERROR}: Indique les informations internes - Erreur lors de
l'appel d'une API, erreur interne, \ldots\hspace{0pt}
\item
\textbf{FATAL} (ou \textbf{EXCEPTION}): \ldots\hspace{0pt}
généralement suivie d'une terminaison du programme ;-) - Bind raté
d'un socket, etc.
\end{itemize}
La configuration des \emph{loggers} est relativement simple, un peu plus
complexe si nous nous penchons dessus, et franchement complète si nous
creusons encore. Il est ainsi possible de définir des formattages,
gestionnaires (\emph{handlers}) et loggers distincts, en fonction de nos
applications.
Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter
les informations de notre application comme un flux d'évènements. Il
n'est donc pas réellement nécessaire de chipoter la configuration,
puisque la seule classe qui va réellement nous intéresser concerne les
\texttt{StreamHandler}. La configuration que nous allons utiliser est
celle-ci:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Formattage: à définir - mais la variante suivante est complète,
lisible et pratique:
\texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}}
\item
Handler: juste un, qui définit un \texttt{StreamHandler}
\item
Logger: pour celui-ci, nous avons besoin d'un niveau (\texttt{level})
et de savoir s'il faut propager les informations vers les
sous-paquets, auquel cas il nous suffira de fixer la valeur de
\texttt{propagate} à \texttt{True}.
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{LOGGING }\OperatorTok{=}\NormalTok{ \{}
\StringTok{\textquotesingle{}version\textquotesingle{}}\NormalTok{: }\DecValTok{1}\NormalTok{,}
\StringTok{\textquotesingle{}disable\_existing\_loggers\textquotesingle{}}\NormalTok{: }\VariableTok{False}\NormalTok{,}
\StringTok{\textquotesingle{}formatters\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}verbose\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}format\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}}\SpecialCharTok{\{levelname\}}\StringTok{ }\SpecialCharTok{\{asctime\}}\StringTok{ }\SpecialCharTok{\{module\}}\StringTok{ }\SpecialCharTok{\{process:d\}}\StringTok{ }\SpecialCharTok{\{thread:d\}}\StringTok{ }\SpecialCharTok{\{message\}}\StringTok{\textquotesingle{}}\NormalTok{,}
\NormalTok{ \},}
\StringTok{\textquotesingle{}simple\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}format\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}}\SpecialCharTok{\{levelname\}}\StringTok{ }\SpecialCharTok{\{asctime\}}\StringTok{ }\SpecialCharTok{\{module\}}\StringTok{ }\SpecialCharTok{\{message\}}\StringTok{\textquotesingle{}}\NormalTok{,}
\NormalTok{ \},}
\NormalTok{ \},}
\StringTok{\textquotesingle{}handlers\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}console\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}level\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}DEBUG\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}logging.StreamHandler\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}formatter\textquotesingle{}}\NormalTok{: }\StringTok{"verbose"}
\NormalTok{ \}}
\NormalTok{ \},}
\StringTok{\textquotesingle{}loggers\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}khana\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}handlers\textquotesingle{}}\NormalTok{: [}\StringTok{\textquotesingle{}console\textquotesingle{}}\NormalTok{],}
\StringTok{\textquotesingle{}level\textquotesingle{}}\NormalTok{: env(}\StringTok{"LOG\_LEVEL"}\NormalTok{, default}\OperatorTok{=}\StringTok{"DEBUG"}\NormalTok{),}
\StringTok{\textquotesingle{}propagate\textquotesingle{}}\NormalTok{: }\VariableTok{True}\NormalTok{,}
\NormalTok{ \},}
\NormalTok{ \}}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
Pour utiliser nos loggers, il suffit de copier le petit bout de code
suivant:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{import}\NormalTok{ logging}
\NormalTok{logger }\OperatorTok{=}\NormalTok{ logging.getLogger(}\VariableTok{\_\_name\_\_}\NormalTok{)}
\NormalTok{logger.debug(}\StringTok{\textquotesingle{}helloworld\textquotesingle{}}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
\href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par
exemples}.
\hypertarget{_logging_2}{%
\subsection{Logging}\label{_logging_2}}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Sentry via sentry\_sdk
\item
Nagios
\item
LibreNMS
\item
Zabbix
\end{enumerate}
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}.
\hypertarget{_muxe9thode_de_duxe9ploiement}{%
\section{Méthode de déploiement}\label{_muxe9thode_de_duxe9ploiement}}
Nous allons détailler ci-dessous trois méthodes de déploiement:
\begin{itemize}
\item
Sur une machine hôte, en embarquant tous les composants sur un même
serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de
configurer un \emph{load balancer}, de routeur plusieurs basées de
données, mais ce sera le premier cas de figure.
\item
Dans des containers, avec Docker-Compose.
\item
Sur une \textbf{Plateforme en tant que Service} (ou plus simplement,
\textbf{PaaSPaaS}), pour faire abstraction de toute la couche de
configuration du serveur.
\end{itemize}
\hypertarget{_duxe9ploiement_sur_debian}{%
\section{Déploiement sur Debian}\label{_duxe9ploiement_sur_debian}}
La première étape pour la configuration de notre hôte consiste à définir
les utilisateurs et groupes de droits. Il est faut absolument éviter de
faire tourner une application en tant qu'utilisateur \textbf{root}, car
la moindre faille pourrait avoir des conséquences catastrophiques.
Une fois que ces utilisateurs seront configurés, nous pourrons passer à
l'étape de configuration, qui consistera à:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Déployer les sources
\item
Démarrer un serveur implémentant une interface WSGI (\textbf{Web
Server Gateway Interface}), qui sera chargé de créer autant de petits
lutins travailleurs que nous le désirerons.
\item
Démarrer un superviseur, qui se chargera de veiller à la bonne santé
de nos petits travailleurs, et en créer de nouveaux s'il le juge
nécessaire
\item
Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes
d'un utilisateur externe à la machine hôte vers notre serveur
applicatif, qui la communiquera à l'un des travailleurs.
\end{enumerate}
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH,
Vultr, \ldots\hspace{0pt} Il existe des dizaines d'hébergements typés
VPS (\textbf{Virtual Private Server}). A vous de choisir celui qui vous
convient \footnote{Personnellement, j'ai un petit faible pour Hetzner
Cloud}.
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{apt}\NormalTok{ update}
\ExtensionTok{groupadd}\NormalTok{ {-}{-}system webapps }
\ExtensionTok{groupadd}\NormalTok{ {-}{-}system gunicorn\_sockets }
\ExtensionTok{useradd}\NormalTok{ {-}{-}system {-}{-}gid webapps {-}{-}shell /bin/bash {-}{-}home /home/gwift gwift }
\FunctionTok{mkdir}\NormalTok{ {-}p /home/gwift }
\FunctionTok{chown}\NormalTok{ gwift:webapps /home/gwift }
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
On ajoute un groupe intitulé \texttt{webapps}
\item
On crée un groupe pour les communications via sockets
\item
On crée notre utilisateur applicatif; ses applications seront placées
dans le répertoire \texttt{/home/gwift}
\item
On crée le répertoire home/gwift
\item
On donne les droits sur le répertoire /home/gwift
\end{itemize}
\hypertarget{_installation_des_duxe9pendances_systuxe8mes}{%
\subsection{Installation des dépendances
systèmes}\label{_installation_des_duxe9pendances_systuxe8mes}}
La version 3.6 de Python se trouve dans les dépôts officiels de CentOS.
Si vous souhaitez utiliser une version ultérieure, il suffit de
l'installer en parallèle de la version officiellement supportée par
votre distribution.
Pour CentOS, vous avez donc deux possibilités :
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{yum}\NormalTok{ install python36 {-}y}
\end{Highlighting}
\end{Shaded}
Ou passer par une installation alternative:
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{sudo}\NormalTok{ yum {-}y groupinstall }\StringTok{"Development Tools"}
\FunctionTok{sudo}\NormalTok{ yum {-}y install openssl{-}devel bzip2{-}devel libffi{-}devel}
\FunctionTok{wget}\NormalTok{ https://www.python.org/ftp/python/3.8.2/Python{-}3.8.2.tgz}
\BuiltInTok{cd}\NormalTok{ Python{-}3.8*/}
\ExtensionTok{./configure}\NormalTok{ {-}{-}enable{-}optimizations}
\FunctionTok{sudo}\NormalTok{ make altinstall }
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
\textbf{Attention !} Le paramètre \texttt{altinstall} est primordial.
Sans lui, vous écraserez l'interpréteur initialement supporté par la
distribution, et cela pourrait avoir des effets de bord non souhaités.
\end{itemize}
\hypertarget{_installation_de_la_base_de_donnuxe9es}{%
\subsection{Installation de la base de
données}\label{_installation_de_la_base_de_donnuxe9es}}
On l'a déjà vu, Django se base sur un pattern type
\href{https://www.martinfowler.com/eaaCatalog/activeRecord.html}{ActiveRecords}
pour la gestion de la persistance des données et supporte les principaux
moteurs de bases de données connus:
\begin{itemize}
\item
SQLite (en natif, mais Django 3.0 exige une version du moteur
supérieure ou égale à la 3.8)
\item
MariaDB (en natif depuis Django 3.0),
\item
PostgreSQL au travers de psycopg2 (en natif aussi),
\item
Microsoft SQLServer grâce aux drivers {[}\ldots\hspace{0pt}à
compléter{]}
\item
Oracle via
\href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}.
\end{itemize}
Chaque pilote doit être utilisé précautionneusement ! Chaque version de
Django n'est pas toujours compatible avec chacune des versions des
pilotes, et chaque moteur de base de données nécessite parfois une
version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur
une version de Django, simplement parce que votre serveur de base de
données se trouvera dans une version spécifique (eg. Django 2.3 à cause
d'un Oracle 12.1).
Ci-dessous, quelques procédures d'installation pour mettre un serveur à
disposition. Les deux plus simples seront MariaDB et PostgreSQL, qu'on
couvrira ci-dessous. Oracle et Microsoft SQLServer se trouveront en
annexes.
\hypertarget{_postgresql}{%
\subsubsection{PostgreSQL}\label{_postgresql}}
On commence par installer PostgreSQL.
Par exemple, dans le cas de debian, on exécute la commande suivante:
\begin{Shaded}
\begin{Highlighting}[]
\VariableTok{$$}\NormalTok{$ }\ExtensionTok{aptitude}\NormalTok{ install postgresql postgresql{-}contrib}
\end{Highlighting}
\end{Shaded}
Ensuite, on crée un utilisateur pour la DB:
\begin{Shaded}
\begin{Highlighting}[]
\VariableTok{$$}\NormalTok{$ }\FunctionTok{su}\NormalTok{ {-} postgres}
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ createuser {-}{-}interactive {-}P}
\ExtensionTok{Enter}\NormalTok{ name of role to add: gwift\_user}
\ExtensionTok{Enter}\NormalTok{ password for new role:}
\ExtensionTok{Enter}\NormalTok{ it again:}
\ExtensionTok{Shall}\NormalTok{ the new role be a superuser? (y/n) }\ExtensionTok{n}
\ExtensionTok{Shall}\NormalTok{ the new role be allowed to create databases? (y/n) }\ExtensionTok{n}
\ExtensionTok{Shall}\NormalTok{ the new role be allowed to create more new roles? (y/n) }\ExtensionTok{n}
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$}
\end{Highlighting}
\end{Shaded}
Finalement, on peut créer la DB:
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ createdb {-}{-}owner gwift\_user gwift}
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ exit}
\BuiltInTok{logout}
\VariableTok{$$}\NormalTok{$}
\end{Highlighting}
\end{Shaded}
penser à inclure un bidule pour les backups.
\hypertarget{_mariadb}{%
\subsubsection{MariaDB}\label{_mariadb}}
Idem, installation, configuration, backup, tout ça. A copier de
grimboite, je suis sûr d'avoir des notes là-dessus.
\hypertarget{_microsoft_sql_server}{%
\subsubsection{Microsoft SQL Server}\label{_microsoft_sql_server}}
\hypertarget{_oracle}{%
\subsubsection{Oracle}\label{_oracle}}
\hypertarget{_pruxe9paration_de_lenvironnement_utilisateur}{%
\subsection{Préparation de l'environnement
utilisateur}\label{_pruxe9paration_de_lenvironnement_utilisateur}}
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{su}\NormalTok{ {-} gwift}
\FunctionTok{cp}\NormalTok{ /etc/skel/.bashrc .}
\FunctionTok{cp}\NormalTok{ /etc/skel/.bash\_profile .}
\FunctionTok{ssh{-}keygen}
\FunctionTok{mkdir}\NormalTok{ bin}
\FunctionTok{mkdir}\NormalTok{ .venvs}
\FunctionTok{mkdir}\NormalTok{ webapps}
\ExtensionTok{python3.6}\NormalTok{ {-}m venv .venvs/gwift}
\BuiltInTok{source}\NormalTok{ .venvs/gwift/bin/activate}
\BuiltInTok{cd}\NormalTok{ /home/gwift/webapps}
\FunctionTok{git}\NormalTok{ clone ...}
\end{Highlighting}
\end{Shaded}
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de
pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en
démarrant les commandes suivantes:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# en tant qu\textquotesingle{}utilisateur \textquotesingle{}gwift\textquotesingle{}}
\BuiltInTok{source}\NormalTok{ .venvs/gwift/bin/activate}
\ExtensionTok{pip}\NormalTok{ install {-}U pip}
\ExtensionTok{pip}\NormalTok{ install {-}r requirements/base.txt}
\ExtensionTok{pip}\NormalTok{ install gunicorn}
\BuiltInTok{cd}\NormalTok{ webapps/gwift}
\ExtensionTok{gunicorn}\NormalTok{ config.wsgi:application {-}{-}bind localhost:3000 {-}{-}settings=config.settings\_production}
\end{Highlighting}
\end{Shaded}
\hypertarget{_configuration_de_lapplication}{%
\subsection{Configuration de
l'application}\label{_configuration_de_lapplication}}
\begin{Shaded}
\begin{Highlighting}[]
\VariableTok{SECRET\_KEY=}\OperatorTok{\textless{}}\KeywordTok{set} \ExtensionTok{your}\NormalTok{ secret key here}\OperatorTok{\textgreater{}}
\VariableTok{ALLOWED\_HOSTS=}\ExtensionTok{*}
\VariableTok{STATIC\_ROOT=}\NormalTok{/var/www/gwift/static}
\VariableTok{DATABASE=}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
La variable \texttt{SECRET\_KEY} est notamment utilisée pour le
chiffrement des sessions.
\item
On fait confiance à django\_environ pour traduire la chaîne de
connexion à la base de données.
\end{itemize}
\hypertarget{_cruxe9ation_des_ruxe9pertoires_de_logs}{%
\subsection{Création des répertoires de
logs}\label{_cruxe9ation_des_ruxe9pertoires_de_logs}}
\begin{verbatim}
mkdir -p /var/www/gwift/static
\end{verbatim}
\hypertarget{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}{%
\subsection{Création du répertoire pour le
socket}\label{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}}
Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}:
\begin{verbatim}
D /var/run/webapps 0775 gwift gunicorn_sockets -
\end{verbatim}
Suivi de la création par systemd :
\begin{verbatim}
systemd-tmpfiles --create
\end{verbatim}
\hypertarget{_gunicorn}{%
\subsection{Gunicorn}\label{_gunicorn}}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\#!/bin/bash}
\CommentTok{\# defines settings for gunicorn}
\VariableTok{NAME=}\StringTok{"gwift"}
\VariableTok{DJANGODIR=}\NormalTok{/home/gwift/webapps/gwift}
\VariableTok{SOCKFILE=}\NormalTok{/var/run/webapps/gunicorn\_gwift.sock}
\VariableTok{USER=}\NormalTok{gwift}
\VariableTok{GROUP=}\NormalTok{gunicorn\_sockets}
\VariableTok{NUM\_WORKERS=}\NormalTok{5}
\VariableTok{DJANGO\_SETTINGS\_MODULE=}\NormalTok{config.settings\_production}
\VariableTok{DJANGO\_WSGI\_MODULE=}\NormalTok{config.wsgi}
\BuiltInTok{echo} \StringTok{"Starting }\VariableTok{$NAME}\StringTok{ as }\KeywordTok{\textasciigrave{}}\FunctionTok{whoami}\KeywordTok{\textasciigrave{}}\StringTok{"}
\BuiltInTok{source}\NormalTok{ /home/gwift/.venvs/gwift/bin/activate}
\BuiltInTok{cd} \VariableTok{$DJANGODIR}
\BuiltInTok{export} \VariableTok{DJANGO\_SETTINGS\_MODULE=$DJANGO\_SETTINGS\_MODULE}
\BuiltInTok{export} \VariableTok{PYTHONPATH=$DJANGODIR}\NormalTok{:}\VariableTok{$PYTHONPATH}
\BuiltInTok{exec}\NormalTok{ gunicorn }\VariableTok{$\{DJANGO\_WSGI\_MODULE\}}\NormalTok{:application }\KeywordTok{\textbackslash{}}
\ExtensionTok{{-}{-}name} \VariableTok{$NAME} \KeywordTok{\textbackslash{}}
\ExtensionTok{{-}{-}workers} \VariableTok{$NUM\_WORKERS} \KeywordTok{\textbackslash{}}
\ExtensionTok{{-}{-}user} \VariableTok{$USER} \KeywordTok{\textbackslash{}}
\ExtensionTok{{-}{-}bind}\NormalTok{=unix:}\VariableTok{$SOCKFILE} \KeywordTok{\textbackslash{}}
\ExtensionTok{{-}{-}log{-}level}\NormalTok{=debug }\KeywordTok{\textbackslash{}}
\ExtensionTok{{-}{-}log{-}file}\NormalTok{={-}}
\end{Highlighting}
\end{Shaded}
\hypertarget{_supervision_keep_alive_et_autoreload}{%
\subsection{Supervision, keep-alive et
autoreload}\label{_supervision_keep_alive_et_autoreload}}
Pour la supervision, on passe par Supervisor. Il existe d'autres
superviseurs,
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{yum}\NormalTok{ install supervisor {-}y}
\end{Highlighting}
\end{Shaded}
On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{[}\ExtensionTok{program}\NormalTok{:gwift]}
\VariableTok{command=}\NormalTok{/home/gwift/bin/start\_gunicorn.sh}
\VariableTok{user=}\NormalTok{gwift}
\VariableTok{stdout\_logfile=}\NormalTok{/var/log/gwift/gwift.log}
\VariableTok{autostart=}\NormalTok{true}
\VariableTok{autorestart=}\NormalTok{unexpected}
\VariableTok{redirect\_stdout=}\NormalTok{true}
\VariableTok{redirect\_stderr=}\NormalTok{true}
\end{Highlighting}
\end{Shaded}
Et on crée les répertoires de logs, on démarre supervisord et on vérifie
qu'il tourne correctement:
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{mkdir}\NormalTok{ /var/log/gwift}
\FunctionTok{chown}\NormalTok{ gwift:nagios /var/log/gwift}
\ExtensionTok{systemctl}\NormalTok{ enable supervisord}
\ExtensionTok{systemctl}\NormalTok{ start supervisord.service}
\ExtensionTok{systemctl}\NormalTok{ status supervisord.service}
\NormalTok{}\ExtensionTok{supervisord.service}\NormalTok{ {-} Process Monitoring and Control Daemon}
\ExtensionTok{Loaded}\NormalTok{: loaded (/usr/lib/systemd/system/supervisord.service}\KeywordTok{;} \ExtensionTok{enabled}\KeywordTok{;} \ExtensionTok{vendor}\NormalTok{ preset: disabled)}
\ExtensionTok{Active}\NormalTok{: active (running) }\ExtensionTok{since}\NormalTok{ Tue 2019{-}12{-}24 10:08:09 CET}\KeywordTok{;} \ExtensionTok{10s}\NormalTok{ ago}
\ExtensionTok{Process}\NormalTok{: 2304 ExecStart=/usr/bin/supervisord {-}c /etc/supervisord.conf (code=exited, status=0/SUCCESS)}
\ExtensionTok{Main}\NormalTok{ PID: 2310 (supervisord)}
\ExtensionTok{CGroup}\NormalTok{: /system.slice/supervisord.service}
\NormalTok{ ├─}\ExtensionTok{2310}\NormalTok{ /usr/bin/python /usr/bin/supervisord {-}c /etc/supervisord.conf}
\NormalTok{ ├─}\ExtensionTok{2313}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...}
\NormalTok{ ├─}\ExtensionTok{2317}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...}
\NormalTok{ ├─}\ExtensionTok{2318}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...}
\NormalTok{ ├─}\ExtensionTok{2321}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...}
\NormalTok{ ├─}\ExtensionTok{2322}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...}
\NormalTok{ └─}\ExtensionTok{2323}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...}
\FunctionTok{ls}\NormalTok{ /var/run/webapps/}
\end{Highlighting}
\end{Shaded}
On peut aussi vérifier que l'application est en train de tourner, à
l'aide de la commande \texttt{supervisorctl}:
\begin{Shaded}
\begin{Highlighting}[]
\VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ status gwift}
\ExtensionTok{gwift}\NormalTok{ RUNNING pid 31983, uptime 0:01:00}
\end{Highlighting}
\end{Shaded}
Et pour gérer le démarrage ou l'arrêt, on peut passer par les commandes
suivantes:
\begin{Shaded}
\begin{Highlighting}[]
\VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ stop gwift}
\ExtensionTok{gwift}\NormalTok{: stopped}
\ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl start gwift}
\ExtensionTok{gwift}\NormalTok{: started}
\ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl restart gwift}
\ExtensionTok{gwift}\NormalTok{: stopped}
\ExtensionTok{gwift}\NormalTok{: started}
\end{Highlighting}
\end{Shaded}
\hypertarget{_configuration_du_firewall_et_ouverture_des_ports}{%
\subsection{Configuration du firewall et ouverture des
ports}\label{_configuration_du_firewall_et_ouverture_des_ports}}
\begin{verbatim}
et 443 (HTTPS).
\end{verbatim}
\begin{verbatim}
firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload
\end{verbatim}
\begin{itemize}
\item
On ouvre le port 80, uniquement pour autoriser une connexion HTTP,
mais qui sera immédiatement redirigée vers HTTPS
\item
Et le port 443 (forcément).
\end{itemize}
\hypertarget{_installation_dnginx}{%
\subsection{Installation d'Nginx}\label{_installation_dnginx}}
\begin{verbatim}
yum install nginx -y
usermod -a -G gunicorn_sockets nginx
\end{verbatim}
On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}:
\begin{verbatim}
upstream gwift_app {
server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0;
}
server {
listen 80;
server_name <server_name>;
root /var/www/gwift;
error_log /var/log/nginx/gwift_error.log;
access_log /var/log/nginx/gwift_access.log;
client_max_body_size 4G;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /static/ {
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_app;
}
}
\end{verbatim}
\begin{itemize}
\item
Ce répertoire sera complété par la commande \texttt{collectstatic} que
l'on verra plus tard. L'objectif est que les fichiers ne demandant
aucune intelligence soit directement servis par Nginx. Cela évite
d'avoir un processus Python (relativement lent) qui doive être
instancié pour servir un simple fichier statique.
\item
Afin d'éviter que Django ne reçoive uniquement des requêtes provenant
de 127.0.0.1
\end{itemize}
\hypertarget{_mise_uxe0_jour}{%
\subsection{Mise à jour}\label{_mise_uxe0_jour}}
Script de mise à jour.
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{su}\NormalTok{ {-} }\OperatorTok{\textless{}}\NormalTok{user}\OperatorTok{\textgreater{}}
\BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}}\NormalTok{/bin/activate}
\BuiltInTok{cd}\NormalTok{ \textasciitilde{}/webapps/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}}
\FunctionTok{git}\NormalTok{ fetch}
\FunctionTok{git}\NormalTok{ checkout vX.Y.Z}
\ExtensionTok{pip}\NormalTok{ install {-}U requirements/prod.txt}
\ExtensionTok{python}\NormalTok{ manage.py migrate}
\ExtensionTok{python}\NormalTok{ manage.py collectstatic}
\BuiltInTok{kill}\NormalTok{ {-}HUP }\KeywordTok{\textasciigrave{}}\FunctionTok{ps}\NormalTok{ {-}C gunicorn fch {-}o pid }\KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}n 1}\KeywordTok{\textasciigrave{}}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
\url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file}
\end{itemize}
\hypertarget{_configuration_des_sauvegardes}{%
\subsection{Configuration des
sauvegardes}\label{_configuration_des_sauvegardes}}
Les sauvegardes ont été configurées avec borg:
\texttt{yum\ install\ borgbackup}.
C'est l'utilisateur gwift qui s'en occupe.
\begin{verbatim}
mkdir -p /home/gwift/borg-backups/
cd /home/gwift/borg-backups/
borg init gwift.borg -e=none
borg create gwift.borg::{now} ~/bin ~/webapps
\end{verbatim}
Et dans le fichier crontab :
\begin{verbatim}
0 23 * * * /home/gwift/bin/backup.sh
\end{verbatim}
\hypertarget{_rotation_des_jounaux}{%
\subsection{Rotation des jounaux}\label{_rotation_des_jounaux}}
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{/var/log/gwift/*}\NormalTok{ \{}
\ExtensionTok{weekly}
\ExtensionTok{rotate}\NormalTok{ 3}
\FunctionTok{size}\NormalTok{ 10M}
\ExtensionTok{compress}
\ExtensionTok{delaycompress}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift
pour vérifier que cela fonctionne correctement.
\hypertarget{_ansible}{%
\subsection{Ansible}\label{_ansible}}
TODO
\hypertarget{_duxe9ploiement_sur_heroku}{%
\section{Déploiement sur Heroku}\label{_duxe9ploiement_sur_heroku}}
\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A
Service} paas, où vous choisissez le \emph{service} dont vous avez
besoin (une base de données, un service de cache, un service applicatif,
\ldots\hspace{0pt}), vous lui envoyer les paramètres nécessaires et le
tout démarre gentiment sans que vous ne deviez superviser l'hôte. Ce
mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà
parlé plus tôt - raison de plus pour que notre application soit
directement prête à y être déployée, d'autant plus qu'il ne sera pas
possible de modifier un fichier une fois qu'elle aura démarré: si vous
souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et
envoyer de nouveaux paramètres et recommencer le déploiement depuis le
début.
\begin{figure}
\centering
\includegraphics{images/deployment/heroku.png}
\caption{Invest in apps, not ops. Heroku handles the hard stuff ---
patching and upgrading, 24/7 ops and security, build systems, failovers,
and more --- so your developers can stay focused on building great
apps.}
\end{figure}
Pour un projet de type "hobby" et pour l'exemple de déploiement
ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un
kopek, afin de tester nos quelques idées ou mettre rapidement un
\emph{Most Valuable Product} en place. La seule contrainte consistera à
pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra
être fait en configurant un \emph{bucket compatible S3}, par exemple
chez Amazon, Scaleway ou OVH.
Le fonctionnement est relativement simple: pour chaque application,
Heroku crée un dépôt Git qui lui est associé. Il suffit donc d'envoyer
les sources de votre application vers ce dépôt pour qu'Heroku les
interprête comme étant une nouvelle version, déploie les nouvelles
fonctionnalités - sous réserve que tous les tests passent correctement -
et les mettent à disposition. Dans un fonctionnement plutôt manuel,
chaque déploiement est initialisé par le développeur ou par un membre de
l'équipe. Dans une version plus automatisée, chacun de ces déploiements
peut être placé en fin de \emph{pipeline}, lorsque tous les tests
unitaires et d'intégration auront été réalisés.
Au travers de la commande \texttt{heroku\ create}, vous associez donc
une nouvelle référence à votre code source, comme le montre le contenu
du fichier \texttt{.git/config} ci-dessous:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create}
\ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098}
\ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git}
\NormalTok{$ }\FunctionTok{cat}\NormalTok{ .git/config}
\NormalTok{[}\ExtensionTok{core}\NormalTok{]}
\ExtensionTok{repositoryformatversion}\NormalTok{ = 0}
\ExtensionTok{filemode}\NormalTok{ = false}
\ExtensionTok{bare}\NormalTok{ = false}
\ExtensionTok{logallrefupdates}\NormalTok{ = true}
\ExtensionTok{symlinks}\NormalTok{ = false}
\ExtensionTok{ignorecase}\NormalTok{ = true}
\NormalTok{[}\ExtensionTok{remote} \StringTok{"heroku"}\NormalTok{]}
\ExtensionTok{url}\NormalTok{ = https://git.heroku.com/still{-}thicket{-}66406.git}
\ExtensionTok{fetch}\NormalTok{ = +refs/heads/*:refs/remotes/heroku/*}
\end{Highlighting}
\end{Shaded}
IMPORTANT:
\begin{verbatim}
Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration.
Celle-ci se limite aux deux fichiers suivants:
* Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI
* Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`)
\end{verbatim}
Après ce paramétrage, il suffit de pousser les changements vers ce
nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}.
Heroku propose des espaces de déploiements, mais pas d'espace de
stockage. Il est possible d'y envoyer des fichiers utilisateurs
(typiquement, des media personnalisés), mais ceux-ci seront perdus lors
du redémarrage du container. Il est donc primordial de configurer
correctement l'hébergement des fichiers média, de préférences sur un
stockage compatible S3. s3
Prêt à vous lancer ? Commencez par créer un compte:
\url{https://signup.heroku.com/python}.
\hypertarget{_configuration_du_compte_heroku}{%
\subsection{Configuration du compte
Heroku}\label{_configuration_du_compte_heroku}}
+ Récupération des valeurs d'environnement pour les réutiliser
ci-dessous.
Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre
première application; heureusement, la documentation est super bien
faite:
\begin{figure}
\centering
\includegraphics{images/deployment/heroku-new-app.png}
\caption{Heroku: Commencer à travailler avec un langage}
\end{figure}
Installez ensuite la CLI (\emph{Command Line Interface}) en suivant
\href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation
suivante}.
Au besoin, cette CLI existe pour:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
macOS, \emph{via} `brew `
\item
Windows, grâce à un
\href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la
version 32 bits existe aussi, mais il est peu probable que vous en
ayez besoin)
\item
GNU/Linux, via un script Shell
\texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh}
ou sur \href{https://snapcraft.io/heroku}{SnapCraft}.
\end{enumerate}
Une fois installée, connectez-vous:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ login}
\end{Highlighting}
\end{Shaded}
Et créer votre application:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create}
\ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098}
\ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git}
\end{Highlighting}
\end{Shaded}
\begin{figure}
\centering
\includegraphics{images/deployment/heroku-app-created.png}
\caption{Notre application est à présent configurée!}
\end{figure}
Ajoutons lui une base de données, que nous sauvegarderons à intervalle
régulier:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}postgresql:hobby{-}dev}
\ExtensionTok{Creating}\NormalTok{ heroku{-}postgresql:hobby{-}dev on ⬢ still{-}thicket{-}66406... free}
\ExtensionTok{Database}\NormalTok{ has been created and is available}
\NormalTok{ ! }\ExtensionTok{This}\NormalTok{ database is empty. If upgrading, you can transfer}
\NormalTok{ ! }\ExtensionTok{data}\NormalTok{ from another database with pg:copy}
\ExtensionTok{Created}\NormalTok{ postgresql{-}clear{-}39693 as DATABASE\_URL}
\ExtensionTok{Use}\NormalTok{ heroku addons:docs heroku{-}postgresql to view documentation}
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ pg:backups schedule {-}{-}at }\StringTok{\textquotesingle{}14:00 Europe/Brussels\textquotesingle{}}\NormalTok{ DATABASE\_URL}
\ExtensionTok{Scheduling}\NormalTok{ automatic daily backups of postgresql{-}clear{-}39693 at 14:00 Europe/Brussels... done}
\end{Highlighting}
\end{Shaded}
TODO: voir comment récupérer le backup de la db :-p
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# Copié/collé de https://cookiecutter{-}django.readthedocs.io/en/latest/deployment{-}on{-}heroku.html}
\ExtensionTok{heroku}\NormalTok{ create {-}{-}buildpack https://github.com/heroku/heroku{-}buildpack{-}python}
\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}redis:hobby{-}dev}
\ExtensionTok{heroku}\NormalTok{ addons:create mailgun:starter}
\ExtensionTok{heroku}\NormalTok{ config:set PYTHONHASHSEED=random}
\ExtensionTok{heroku}\NormalTok{ config:set WEB\_CONCURRENCY=4}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_DEBUG=False}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SETTINGS\_MODULE=config.settings.production}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SECRET\_KEY=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 64}\VariableTok{)}\StringTok{"}
\CommentTok{\# Generating a 32 character{-}long random string without any of the visually similar characters "IOl01":}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ADMIN\_URL=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 4096 }\KeywordTok{|} \FunctionTok{tr}\NormalTok{ {-}dc }\StringTok{\textquotesingle{}A{-}HJ{-}NP{-}Za{-}km{-}z2{-}9\textquotesingle{}} \KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}c 32}\VariableTok{)}\StringTok{/"}
\CommentTok{\# Set this to your Heroku app url, e.g. \textquotesingle{}bionic{-}beaver{-}28392.herokuapp.com\textquotesingle{}}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ALLOWED\_HOSTS=}
\CommentTok{\# Assign with AWS\_ACCESS\_KEY\_ID}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_ACCESS\_KEY\_ID=}
\CommentTok{\# Assign with AWS\_SECRET\_ACCESS\_KEY}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_SECRET\_ACCESS\_KEY=}
\CommentTok{\# Assign with AWS\_STORAGE\_BUCKET\_NAME}
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_STORAGE\_BUCKET\_NAME=}
\FunctionTok{git}\NormalTok{ push heroku master}
\ExtensionTok{heroku}\NormalTok{ run python manage.py createsuperuser}
\ExtensionTok{heroku}\NormalTok{ run python manage.py check {-}{-}deploy}
\ExtensionTok{heroku}\NormalTok{ open}
\end{Highlighting}
\end{Shaded}
\hypertarget{_configuration}{%
\subsection{Configuration}\label{_configuration}}
Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les
commandes à exécuter pour que tout fonctionne correctement. Pour un
projet Django, cela comprend, à placer à la racine de votre projet:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Un fichier \texttt{requirements.txt} (qui peut éventuellement faire
appel à un autre fichier, \textbf{via} l'argument \texttt{-r})
\item
Un fichier \texttt{Procfile} ({[}sans
extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!),
qui expliquera la commande pour le protocole WSGI.
\end{enumerate}
Dans notre exemple:
\begin{verbatim}
# requirements.txt
django==3.2.8
gunicorn
boto3
django-storages
\end{verbatim}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# Procfile}
\ExtensionTok{release}\NormalTok{: python3 manage.py migrate}
\ExtensionTok{web}\NormalTok{: gunicorn gwift.wsgi}
\end{Highlighting}
\end{Shaded}
\hypertarget{_huxe9bergement_s3}{%
\subsection{Hébergement S3}\label{_huxe9bergement_s3}}
Pour cette partie, nous allons nous baser sur
l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de
Scaleway}. Ils offrent 75GB de stockage et de transfert par mois, ce qui
va nous laisser suffisament d'espace pour jouer un peu 😉.
\includegraphics{images/deployment/scaleway-object-storage-bucket.png}
L'idée est qu'au moment de la construction des fichiers statiques,
Django aille simplement les héberger sur un espace de stockage
compatible S3. La complexité va être de configurer correctement les
différents points de terminaison. Pour héberger nos fichiers sur notre
\textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes
dans l'ordre:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y
en a - \textbf{littéralement} - des dizaines.
\item
Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec
ce type de protocole
\item
Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de
faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et
Django, qui s'attend à ce qu'on lui donne un moteur de gestion
\textbf{via} la clé
{[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}).
\end{enumerate}
La première étape consiste à se rendre dans {[}la console
Scaleway{]}(\url{https://console.scaleway.com/project/credentials}),
pour gérer ses identifiants et créer un jeton.
\includegraphics{images/deployment/scaleway-api-key.png}
Selon la documentation de
\href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages},
de
\href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3}
et de
\href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway},
vous aurez besoin des clés suivantes au niveau du fichier
\texttt{settings.py}:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{AWS\_ACCESS\_KEY\_ID }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}ACCESS\_KEY\_ID\textquotesingle{}}\NormalTok{)}
\NormalTok{AWS\_SECRET\_ACCESS\_KEY }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}SECRET\_ACCESS\_KEY\textquotesingle{}}\NormalTok{)}
\NormalTok{AWS\_STORAGE\_BUCKET\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_STORAGE\_BUCKET\_NAME\textquotesingle{}}\NormalTok{)}
\NormalTok{AWS\_S3\_REGION\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_S3\_REGION\_NAME\textquotesingle{}}\NormalTok{)}
\NormalTok{AWS\_DEFAULT\_ACL }\OperatorTok{=} \StringTok{\textquotesingle{}public{-}read\textquotesingle{}}
\NormalTok{AWS\_LOCATION }\OperatorTok{=} \StringTok{\textquotesingle{}static\textquotesingle{}}
\NormalTok{AWS\_S3\_SIGNATURE\_VERSION }\OperatorTok{=} \StringTok{\textquotesingle{}s3v4\textquotesingle{}}
\NormalTok{AWS\_S3\_HOST }\OperatorTok{=} \StringTok{\textquotesingle{}s3.}\SpecialCharTok{\%s}\StringTok{.scw.cloud\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_REGION\_NAME,)}
\NormalTok{AWS\_S3\_ENDPOINT\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}https://}\SpecialCharTok{\%s}\StringTok{\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_HOST, )}
\NormalTok{DEFAULT\_FILE\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3Boto3Storage\textquotesingle{}}
\NormalTok{STATICFILES\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3ManifestStaticStorage\textquotesingle{}}
\NormalTok{STATIC\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}}\SpecialCharTok{\%s}\StringTok{/}\SpecialCharTok{\%s}\StringTok{/\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_ENDPOINT\_URL, AWS\_LOCATION)}
\CommentTok{\# General optimization for faster delivery}
\NormalTok{AWS\_IS\_GZIPPED }\OperatorTok{=} \VariableTok{True}
\NormalTok{AWS\_S3\_OBJECT\_PARAMETERS }\OperatorTok{=}\NormalTok{ \{}
\StringTok{\textquotesingle{}CacheControl\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}max{-}age=86400\textquotesingle{}}\NormalTok{,}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
Configurez-les dans la console d'administration d'Heroku:
\includegraphics{images/deployment/heroku-vars-reveal.png}
Lors de la publication, vous devriez à présent avoir la sortie suivante,
qui sera confirmée par le \textbf{bucket}:
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{remote}\NormalTok{: {-}{-}{-}{-}{-}}\OperatorTok{\textgreater{}}\NormalTok{ $ python manage.py collectstatic {-}{-}noinput}
\ExtensionTok{remote}\NormalTok{: 128 static files copied, 156 post{-}processed.}
\end{Highlighting}
\end{Shaded}
\includegraphics{images/deployment/gwift-cloud-s3.png}
Sources complémentaires:
\begin{itemize}
\item
{[}How to store Django static and media files on S3 in
production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/})
\item
{[}Using Django and
Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/})
\end{itemize}
\hypertarget{_docker_compose}{%
\subsection{Docker-Compose}\label{_docker_compose}}
(c/c Ced' - 2020-01-24)
Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter
pour django.
D'abords, après avoir installer docker-compose et les dépendances sous
debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root
pour utiliser docker. Ensuite, j'ai relancé mon pc car juste relancé un
shell n'a pas suffit pour que je puisse utiliser docker avec mon compte.
Bon après c'est facile, un petit virtualenv pour cookiecutter, suivit
d'une installation du template django. Et puis j'ai suivi sans t
\url{https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html}
Alors, il télécharge les images, fait un petit update, installe les
dépendances de dev, install les requirement pip \ldots\hspace{0pt}
Du coup, ça prend vite de la place: image.png
L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque
1,5 GB d'un coup.
Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien
faire ou presque.
La partie ci-dessous a été reprise telle quelle de
\href{https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html}{la
documentation de cookie-cutter-django}.
le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt
source.
On peut aussi passer par fabric, ansible, chef ou puppet.
\hypertarget{_autres_outils}{%
\section{Autres outils}\label{_autres_outils}}
Voir aussi devpi, circus, uswgi, statsd.
See \url{https://mattsegal.dev/nginx-django-reverse-proxy-config.html}
\hypertarget{_ressources}{%
\section{Ressources}\label{_ressources}}
\begin{itemize}
\item
\url{https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/}
\item
\href{https://docs.djangoproject.com/fr/3.0/howto/deployment/}{Déploiement}.
\item
Let's Encrypt !
\end{itemize}
Nous avons fait exprès de reprendre l'acronyme d'une \emph{Services
Oriented Architecture} pour cette partie. L'objectif est de vous mettre
la puce à l'oreille quant à la finalité du développement: que
l'utilisateur soit humain, bot automatique ou client Web, l'objectif est
de fournir des applications résilientes, disponibles et accessibles.
Dans cette partie, nous aborderons les vues, la mise en forme, la mise
en page, la définition d'une interface REST, la définition d'une
interface GraphQL et le routage d'URLs.
\hypertarget{_application_programming_interface}{%
\section{Application Programming
Interface}\label{_application_programming_interface}}
\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs
Django Rest Framework
Expliquer pourquoi une API est intéressante/primordiale/la première
chose à réaliser/le cadet de nos soucis.
Voir peut-être aussi
\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/}
Au niveau du modèle, nous allons partir de quelque chose de très simple:
des personnes, des contrats, des types de contrats, et un service
d'affectation. Quelque chose comme ceci:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# models.py}
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ models}
\KeywordTok{class}\NormalTok{ People(models.Model):}
\NormalTok{ CIVILITY\_CHOICES }\OperatorTok{=}\NormalTok{ (}
\NormalTok{ (}\StringTok{"M"}\NormalTok{, }\StringTok{"Monsieur"}\NormalTok{),}
\NormalTok{ (}\StringTok{"Mme"}\NormalTok{, }\StringTok{"Madame"}\NormalTok{),}
\NormalTok{ (}\StringTok{"Dr"}\NormalTok{, }\StringTok{"Docteur"}\NormalTok{),}
\NormalTok{ (}\StringTok{"Pr"}\NormalTok{, }\StringTok{"Professeur"}\NormalTok{),}
\NormalTok{ (}\StringTok{""}\NormalTok{, }\StringTok{""}\NormalTok{)}
\NormalTok{ )}
\NormalTok{ last\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ first\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ civility }\OperatorTok{=}\NormalTok{ models.CharField(}
\NormalTok{ max\_length}\OperatorTok{=}\DecValTok{3}\NormalTok{,}
\NormalTok{ choices}\OperatorTok{=}\NormalTok{CIVILITY\_CHOICES,}
\NormalTok{ default}\OperatorTok{=}\StringTok{""}
\NormalTok{ )}
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return} \StringTok{"}\SpecialCharTok{\{\}}\StringTok{, }\SpecialCharTok{\{\}}\StringTok{"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}\VariableTok{self}\NormalTok{.last\_name, }\VariableTok{self}\NormalTok{.first\_name)}
\KeywordTok{class}\NormalTok{ Service(models.Model):}
\NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.label}
\KeywordTok{class}\NormalTok{ ContractType(models.Model):}
\NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ short\_label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)}
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.short\_label}
\KeywordTok{class}\NormalTok{ Contract(models.Model):}
\NormalTok{ people }\OperatorTok{=}\NormalTok{ models.ForeignKey(People, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\NormalTok{ date\_begin }\OperatorTok{=}\NormalTok{ models.DateField()}
\NormalTok{ date\_end }\OperatorTok{=}\NormalTok{ models.DateField(blank}\OperatorTok{=}\VariableTok{True}\NormalTok{, null}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
\NormalTok{ contract\_type }\OperatorTok{=}\NormalTok{ models.ForeignKey(ContractType, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\NormalTok{ service }\OperatorTok{=}\NormalTok{ models.ForeignKey(Service, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{if} \VariableTok{self}\NormalTok{.date\_end }\KeywordTok{is} \KeywordTok{not} \VariableTok{None}\NormalTok{:}
\ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, jusqu\textquotesingle{}au }\SpecialCharTok{\{\}}\StringTok{, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.date\_begin,}
\VariableTok{self}\NormalTok{.date\_end,}
\VariableTok{self}\NormalTok{.service,}
\VariableTok{self}\NormalTok{.contract\_type}
\NormalTok{ )}
\ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, à durée indéterminée, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
\VariableTok{self}\NormalTok{.date\_begin,}
\VariableTok{self}\NormalTok{.service,}
\VariableTok{self}\NormalTok{.contract\_type}
\NormalTok{ )}
\end{Highlighting}
\end{Shaded}
\includegraphics{images/rest/models.png}
\hypertarget{_configuration_2}{%
\section{Configuration}\label{_configuration_2}}
La configuration des points de terminaison de notre API est relativement
touffue. Il convient de:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Configurer les sérialiseurs, càd. les champs que nous souhaitons
exposer au travers de l'API,
\item
Configurer les vues, càd le comportement de chacun des points de
terminaison,
\item
Configurer les points de terminaison eux-mêmes, càd les URLs
permettant d'accéder aux ressources.
\item
Et finalement ajouter quelques paramètres au niveau de notre
application.
\end{enumerate}
\hypertarget{_suxe9rialiseurs}{%
\subsection{Sérialiseurs}\label{_suxe9rialiseurs}}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# serializers.py}
\ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group}
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ serializers}
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service}
\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ People}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)}
\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)}
\KeywordTok{class}\NormalTok{ ServiceSerializer(serializers.HyperlinkedModelSerializer):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Service}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"name"}\NormalTok{,)}
\end{Highlighting}
\end{Shaded}
\hypertarget{_vues}{%
\subsection{Vues}\label{_vues}}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# views.py}
\ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group}
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets}
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ permissions}
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service}
\ImportTok{from}\NormalTok{ .serializers }\ImportTok{import}\NormalTok{ PeopleSerializer, ContractSerializer, ServiceSerializer}
\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):}
\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ People.objects.}\BuiltInTok{all}\NormalTok{()}
\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ PeopleSerializer}
\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]}
\KeywordTok{class}\NormalTok{ ContractViewSet(viewsets.ModelViewSet):}
\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Contract.objects.}\BuiltInTok{all}\NormalTok{()}
\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ContractSerializer}
\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]}
\KeywordTok{class}\NormalTok{ ServiceViewSet(viewsets.ModelViewSet):}
\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Service.objects.}\BuiltInTok{all}\NormalTok{()}
\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ServiceSerializer}
\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_urls}{%
\subsection{URLs}\label{_urls}}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# urls.py}
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path, include}
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ routers}
\ImportTok{from}\NormalTok{ core }\ImportTok{import}\NormalTok{ views}
\NormalTok{router }\OperatorTok{=}\NormalTok{ routers.DefaultRouter()}
\NormalTok{router.register(}\VerbatimStringTok{r"people"}\NormalTok{, views.PeopleViewSet)}
\NormalTok{router.register(}\VerbatimStringTok{r"contracts"}\NormalTok{, views.ContractViewSet)}
\NormalTok{router.register(}\VerbatimStringTok{r"services"}\NormalTok{, views.ServiceViewSet)}
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ path(}\StringTok{"api/v1/"}\NormalTok{, include(router.urls)),}
\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls),}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_paramuxe8tres}{%
\subsection{Paramètres}\label{_paramuxe8tres}}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# settings.py}
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ ...}
\StringTok{"rest\_framework"}\NormalTok{,}
\NormalTok{ ...}
\NormalTok{]}
\NormalTok{...}
\NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{}
\StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
A ce stade, en nous rendant sur l'URL
\texttt{http://localhost:8000/api/v1}, nous obtiendrons ceci:
\includegraphics{images/rest/api-first-example.png}
\hypertarget{_moduxe8les_et_relations}{%
\section{Modèles et relations}\label{_moduxe8les_et_relations}}
Plus haut, nous avons utilisé une relation de type
\texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour
autoriser des relations entre vos instances à partir de l'API, mais il
faut reconnaître que cela reste assez limité. Pour palier à ceci, il
existe {[}plusieurs manières de représenter ces
relations{]}(\url{https://www.django-rest-framework.org/api-guide/relations/}):
soit \textbf{via} un hyperlien, comme ci-dessus, soit en utilisant les
clés primaires, soit en utilisant l'URL canonique permettant d'accéder à
la ressource. La solution la plus complète consiste à intégrer la
relation directement au niveau des données sérialisées, ce qui nous
permet de passer de ceci (au niveau des contrats):
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{\{}
\DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,}
\DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
\DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
\DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[}
\FunctionTok{\{}
\DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,}
\DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,}
\DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[}
\StringTok{"http://localhost:8000/api/v1/contracts/1/"}\OtherTok{,}
\StringTok{"http://localhost:8000/api/v1/contracts/2/"}
\OtherTok{]}
\FunctionTok{\}}
\OtherTok{]}
\FunctionTok{\}}
\end{Highlighting}
\end{Shaded}
à ceci:
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{\{}
\DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,}
\DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
\DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
\DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[}
\FunctionTok{\{}
\DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,}
\DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,}
\DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[}
\FunctionTok{\{}
\DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2019{-}01{-}01"}\FunctionTok{,}
\DataTypeTok{"date\_end"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
\DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"}
\FunctionTok{\}}\OtherTok{,}
\FunctionTok{\{}
\DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2009{-}01{-}01"}\FunctionTok{,}
\DataTypeTok{"date\_end"}\FunctionTok{:} \StringTok{"2021{-}01{-}01"}\FunctionTok{,}
\DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"}
\FunctionTok{\}}
\OtherTok{]}
\FunctionTok{\}}
\OtherTok{]}
\FunctionTok{\}}
\end{Highlighting}
\end{Shaded}
La modification se limite à \textbf{surcharger} la propriété, pour
indiquer qu'elle consiste en une instance d'un des sérialiseurs
existants. Nous passons ainsi de ceci
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)}
\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ People}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
à ceci:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)}
\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):}
\NormalTok{ contract\_set }\OperatorTok{=}\NormalTok{ ContractSerializer(many}\OperatorTok{=}\VariableTok{True}\NormalTok{, read\_only}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ model }\OperatorTok{=}\NormalTok{ People}
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
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.
\hypertarget{_filtres_et_recherches}{%
\section{Filtres et recherches}\label{_filtres_et_recherches}}
A ce stade, nous pouvons juste récupérer des informations présentes dans
notre base de données, mais à part les parcourir, il est difficile d'en
faire quelque chose.
Il est possible de jouer avec les URLs en définissant une nouvelle route
ou avec les paramètres de l'URL, ce qui demanderait alors de programmer
chaque cas possible - sans que le consommateur ne puisse les déduire
lui-même. Une solution élégante consiste à autoriser le consommateur à
filtrer les données, directement au niveau de l'API. Ceci peut être
fait. Il existe deux manières de restreindre l'ensemble des résultats
retournés:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Soit au travers d'une recherche, qui permet d'effectuer une recherche
textuelle, globale et par ensemble à un ensemble de champs,
\item
Soit au travers d'un filtre, ce qui permet de spécifier une valeur
précise à rechercher.
\end{enumerate}
Dans notre exemple, la première possibilité sera utile pour rechercher
une personne répondant à un ensemble de critères. Typiquement,
\texttt{/api/v1/people/?search=raymond\ bond} ne nous donnera aucun
résultat, alors que \texttt{/api/v1/people/?search=james\ bond} nous
donnera le célèbre agent secret (qui a bien entendu un contrat chez
nous\ldots\hspace{0pt}).
Le second cas permettra par contre de préciser que nous souhaitons
disposer de toutes les personnes dont le contrat est ultérieur à une
date particulière.
Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de
proposer immédiatement les champs, et donc d'informer le consommateur
des possibilités:
\includegraphics{images/rest/drf-filters-and-searches.png}
\hypertarget{_recherches}{%
\subsection{Recherches}\label{_recherches}}
La fonction de recherche est déjà implémentée au niveau de
Django-Rest-Framework, et aucune dépendance supplémentaire n'est
nécessaire. Au niveau du \texttt{viewset}, il suffit d'ajouter deux
informations:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{...}
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ filters, viewsets}
\NormalTok{...}
\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):}
\NormalTok{ ...}
\NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [filters.SearchFilter]}
\NormalTok{ search\_fields }\OperatorTok{=}\NormalTok{ [}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{]}
\NormalTok{ ...}
\end{Highlighting}
\end{Shaded}
\hypertarget{_filtres}{%
\subsection{Filtres}\label{_filtres}}
Nous commençons par installer {[}le paquet
\texttt{django-filter}{]}(\url{https://www.django-rest-framework.org/api-guide/filtering/\#djangofilterbackend})
et nous l'ajoutons parmi les applications installées:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{pip}\NormalTok{ install django{-}filter}
\ExtensionTok{Collecting}\NormalTok{ django{-}filter}
\ExtensionTok{Downloading}\NormalTok{ django\_filter{-}2.4.0{-}py3{-}none{-}any.whl (73 kB)}
\KeywordTok{|}\NormalTok{████████████████████████████████}\KeywordTok{|} \ExtensionTok{73}\NormalTok{ kB 2.6 MB/s}
\ExtensionTok{Requirement}\NormalTok{ already satisfied: Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from django{-}filter) }\KeywordTok{(}\ExtensionTok{3.1.7}\KeywordTok{)}
\ExtensionTok{Requirement}\NormalTok{ already satisfied: asgiref}\OperatorTok{\textless{}}\NormalTok{4,}\OperatorTok{\textgreater{}}\NormalTok{=3.2.10 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{3.3.1}\KeywordTok{)}
\ExtensionTok{Requirement}\NormalTok{ already satisfied: sqlparse}\OperatorTok{\textgreater{}}\NormalTok{=0.2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{0.4.1}\KeywordTok{)}
\ExtensionTok{Requirement}\NormalTok{ already satisfied: pytz in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{2021.1}\KeywordTok{)}
\ExtensionTok{Installing}\NormalTok{ collected packages: django{-}filter}
\ExtensionTok{Successfully}\NormalTok{ installed django{-}filter{-}2.4.0}
\end{Highlighting}
\end{Shaded}
Une fois l'installée réalisée, il reste deux choses à faire:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Ajouter \texttt{django\_filters} parmi les applications installées:
\item
Configurer la clé \texttt{DEFAULT\_FILTER\_BACKENDS} à la valeur
\texttt{{[}\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}{]}}.
\end{enumerate}
Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier
\texttt{settings.py} de la manière suivante:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{}
\StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10}\NormalTok{,}
\StringTok{\textquotesingle{}DEFAULT\_FILTER\_BACKENDS\textquotesingle{}}\NormalTok{: [}\StringTok{\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}}\NormalTok{]}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
Au niveau du viewset, il convient d'ajouter ceci:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{...}
\ImportTok{from}\NormalTok{ django\_filters.rest\_framework }\ImportTok{import}\NormalTok{ DjangoFilterBackend}
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets}
\NormalTok{...}
\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):}
\NormalTok{ ...}
\NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [DjangoFilterBackend]}
\NormalTok{ filterset\_fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}last\_name\textquotesingle{}}\NormalTok{,)}
\NormalTok{ ...}
\end{Highlighting}
\end{Shaded}
A ce stade, nous avons deux problèmes:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le champ que nous avons défini au niveau de la propriété
\texttt{filterset\_fields} exige une correspondance exacte. Ainsi,
\texttt{/api/v1/people/?last\_name=Bon} ne retourne rien, alors que
\texttt{/api/v1/people/?last\_name=Bond} nous donnera notre agent
secret préféré.
\item
Il n'est pas possible d'aller appliquer un critère de sélection sur la
propriété d'une relation. Notre exemple proposant rechercher
uniquement les relations dans le futur (ou dans le passé) tombe à
l'eau.
\end{enumerate}
Pour ces deux points, nous allons définir un nouveau filtre, en
surchargeant une nouvelle classe dont la classe mère serait de type
\texttt{django\_filters.FilterSet}.
TO BE CONTINUED.
A noter qu'il existe un paquet
{[}Django-Rest-Framework-filters{]}(\url{https://github.com/philipn/django-rest-framework-filters}),
mais il est déprécié depuis Django 3.0, puisqu'il se base sur
\texttt{django.utils.six} qui n'existe à présent plus. Il faut donc le
faire à la main (ou patcher le paquet\ldots\hspace{0pt}).
\hypertarget{_urls_et_espaces_de_noms}{%
\section{URLs et espaces de noms}\label{_urls_et_espaces_de_noms}}
La gestion des URLs permet \textbf{grosso modo} d'assigner une adresse
paramétrée ou non à une fonction Python. La manière simple consiste à
modifier le fichier \texttt{gwift/settings.py} pour y ajouter nos
correspondances. Par défaut, le fichier ressemble à ceci:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# gwift/urls.py}
\ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url}
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
La variable \texttt{urlpatterns} associe un ensemble d'adresses à des
fonctions. Dans le fichier \textbf{nu}, seul le \textbf{pattern}
\texttt{admin} est défini, et inclut toutes les adresses qui sont
définies dans le fichier \texttt{admin.site.urls}.
Django fonctionne avec des \textbf{expressions rationnelles} simplifiées
(des \textbf{expressions régulières} ou \textbf{regex}) pour trouver une
correspondance entre une URL et la fonction qui recevra la requête et
retournera une réponse. Nous utilisons l'expression \texttt{\^{}\$} pour
déterminer la racine de notre application, mais nous pourrions appliquer
d'autres regroupements (\texttt{/home},
\texttt{users/\textless{}profile\_id\textgreater{}},
\texttt{articles/\textless{}year\textgreater{}/\textless{}month\textgreater{}/\textless{}day\textgreater{}},
\ldots\hspace{0pt}). Chaque \textbf{variable} déclarée dans l'expression
régulière sera apparenté à un paramètre dans la fonction correspondante.
Ainsi,
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# admin.site.urls.py}
\end{Highlighting}
\end{Shaded}
Pour reprendre l'exemple où on en était resté:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# gwift/urls.py}
\ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url}
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
\ImportTok{from}\NormalTok{ wish }\ImportTok{import}\NormalTok{ views }\ImportTok{as}\NormalTok{ wish\_views}
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),}
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, wish\_views.wishlists, name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
Dans la mesure du possible, essayez toujours de \textbf{nommer} chaque
expression. Cela permettra notamment de les retrouver au travers de la
fonction \texttt{reverse}, mais permettra également de simplifier vos
templates.
A présent, on doit tester que l'URL racine de notre application mène
bien vers la fonction \texttt{wish\_views.wishlists}.
Sauf que les pages \texttt{about} et \texttt{help} existent également.
Pour implémenter ce type de précédence, il faudrait implémenter les URLs
de la manière suivante:
\begin{verbatim}
| about
| help
| <user>
\end{verbatim}
Mais cela signifie aussi que les utilisateurs \texttt{about} et
\texttt{help} (s'ils existent\ldots\hspace{0pt}) ne pourront jamais
accéder à leur profil. Une dernière solution serait de maintenir une
liste d'authorité des noms d'utilisateur qu'il n'est pas possible
d'utiliser.
D'où l'importance de bien définir la séquence de déinition de ces
routes, ainsi que des espaces de noms.
Note sur les namespaces.
De là, découle une autre bonne pratique: l'utilisation de
\emph{breadcrumbs}
(\url{https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template})
ou de guidelines de navigation.
\hypertarget{_reverse}{%
\subsection{Reverse}\label{_reverse}}
En associant un nom ou un libellé à chaque URL, il est possible de
récupérer sa \textbf{traduction}. Cela implique par contre de ne plus
toucher à ce libellé par la suite\ldots\hspace{0pt}
Dans le fichier \texttt{urls.py}, on associe le libellé
\texttt{wishlists} à l'URL \texttt{r\textquotesingle{}\^{}\$}
(c'est-à-dire la racine du site):
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ wish.views }\ImportTok{import}\NormalTok{ WishListList}
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),}
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, WishListList.as\_view(), name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
De cette manière, dans nos templates, on peut à présent construire un
lien vers la racine avec le tags suivant:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{\textless{}a}\OtherTok{ href=}\StringTok{"\{\% url \textquotesingle{}wishlists\textquotesingle{} \%\}"}\KeywordTok{\textgreater{}}\NormalTok{\{\{ yearvar \}\} Archive}\KeywordTok{\textless{}/a\textgreater{}}
\end{Highlighting}
\end{Shaded}
De la même manière, on peut également récupérer l'URL de destination
pour n'importe quel libellé, de la manière suivante:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse\_lazy}
\NormalTok{wishlists\_url }\OperatorTok{=}\NormalTok{ reverse\_lazy(}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
\hypertarget{_i18n_l10n}{%
\section{i18n / l10n}\label{_i18n_l10n}}
La localisation (\emph{l10n}) et l'internationalization (\emph{i18n})
sont deux concepts proches, mais différents:
\begin{itemize}
\item
Internationalisation: \emph{Preparing the software for localization.
Usually done by developers.}
\item
Localisation: \emph{Writing the translations and local formats.
Usually done by translators.}
\end{itemize}
L'internationalisation est donc le processus permettant à une
application d'accepter une forme de localisation. La seconde ne va donc
pas sans la première, tandis que la première ne fait qu'autoriser la
seconde.
\hypertarget{_arborescences}{%
\subsection{Arborescences}\label{_arborescences}}
\begin{Shaded}
\begin{Highlighting}[]
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# \textless{}app\textgreater{}/management/commands/rebuild.py}
\CommentTok{"""This command manages Closure Tables implementation}
\CommentTok{It adds new levels and cleans links between entities.}
\CommentTok{This way, it\textquotesingle{}s relatively easy to fetch an entire tree with just one tiny request.}
\CommentTok{"""}
\ImportTok{from}\NormalTok{ django.core.management.base }\ImportTok{import}\NormalTok{ BaseCommand}
\ImportTok{from}\NormalTok{ rps.structure.models }\ImportTok{import}\NormalTok{ Entity, EntityTreePath}
\KeywordTok{class}\NormalTok{ Command(BaseCommand):}
\KeywordTok{def}\NormalTok{ handle(}\VariableTok{self}\NormalTok{, }\OperatorTok{*}\NormalTok{args, }\OperatorTok{**}\NormalTok{options):}
\NormalTok{ entities }\OperatorTok{=}\NormalTok{ Entity.objects.}\BuiltInTok{all}\NormalTok{()}
\ControlFlowTok{for}\NormalTok{ entity }\KeywordTok{in}\NormalTok{ entities:}
\NormalTok{ breadcrumb }\OperatorTok{=}\NormalTok{ [node }\ControlFlowTok{for}\NormalTok{ node }\KeywordTok{in}\NormalTok{ entity.breadcrumb()]}
\NormalTok{ tree }\OperatorTok{=} \BuiltInTok{set}\NormalTok{(EntityTreePath.objects.}\BuiltInTok{filter}\NormalTok{(descendant}\OperatorTok{=}\NormalTok{entity))}
\ControlFlowTok{for}\NormalTok{ idx, node }\KeywordTok{in} \BuiltInTok{enumerate}\NormalTok{(breadcrumb):}
\NormalTok{ tree\_path, \_ }\OperatorTok{=}\NormalTok{ EntityTreePath.objects.get\_or\_create(}
\NormalTok{ ancestor}\OperatorTok{=}\NormalTok{node, descendant}\OperatorTok{=}\NormalTok{entity, weight}\OperatorTok{=}\NormalTok{idx }\OperatorTok{+} \DecValTok{1}
\NormalTok{ )}
\ControlFlowTok{if}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:}
\NormalTok{ tree.remove(tree\_path)}
\ControlFlowTok{for}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:}
\NormalTok{ tree\_path.delete()}
\end{Highlighting}
\end{Shaded}
\hypertarget{_conclusions_3}{%
\section{Conclusions}\label{_conclusions_3}}
De part son pattern \texttt{MVT}, Django ne fait pas comme les autres
frameworks.
Pour commencer, nous allons nous concentrer sur la création d'un site ne
contenant qu'une seule application, même si en pratique le site
contiendra déjà plusieurs applications fournies pas django, comme nous
le verrons plus loin.
Don't make me think, or why I switched from JS SPAs to Ruby On Rails
\url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment}
Pour prendre un exemple concret, nous allons créer un site permettant de
gérer des listes de souhaits, que nous appellerons \texttt{gwift} (pour
\texttt{GiFTs\ and\ WIshlisTs} :)).
La première chose à faire est de définir nos besoins du point de vue de
l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur
puisse faire avec l'application.
Ensuite, nous pourrons traduire ces besoins en fonctionnalités et
finalement effectuer le développement.
\hypertarget{_gwift}{%
\section{Gwift}\label{_gwift}}
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-gwift.png}
\caption{Gwift}
\end{figure}
\hypertarget{_besoins_utilisateurs}{%
\section{Besoins utilisateurs}\label{_besoins_utilisateurs}}
Nous souhaitons développer un site où un utilisateur donné peut créer
une liste contenant des souhaits et où d'autres utilisateurs,
authentifiés ou non, peuvent choisir les souhaits à la réalisation
desquels ils souhaitent participer.
Il sera nécessaire de s'authentifier pour :
\begin{itemize}
\item
Créer une liste associée à l'utilisateur en cours
\item
Ajouter un nouvel élément à une liste
\end{itemize}
Il ne sera pas nécessaire de s'authentifier pour :
\begin{itemize}
\item
Faire une promesse d'offre pour un élément appartenant à une liste,
associée à un utilisateur.
\end{itemize}
L'utilisateur ayant créé une liste pourra envoyer un email directement
depuis le site aux personnes avec qui il souhaite partager sa liste, cet
email contenant un lien permettant d'accéder à cette liste.
A chaque souhait, on pourrait de manière facultative ajouter un prix.
Dans ce cas, le souhait pourrait aussi être subdivisé en plusieurs
parties, de manière à ce que plusieurs personnes puissent participer à
sa réalisation.
Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à
dupliquer le souhait en question.
\hypertarget{_besoins_fonctionnels}{%
\section{Besoins fonctionnels}\label{_besoins_fonctionnels}}
\hypertarget{_gestion_des_utilisateurs}{%
\subsection{Gestion des utilisateurs}\label{_gestion_des_utilisateurs}}
Pour gérer les utilisateurs, nous allons faire en sorte de surcharger ce
que Django propose: par défaut, on a une la possibilité de gérer des
utilisateurs (identifiés par une adresse email, un nom, un prénom,
\ldots\hspace{0pt}) mais sans plus.
Ce qu'on peut souhaiter, c'est que l'utilisateur puisse s'authentifier
grâce à une plateforme connue (Facebook, Twitter, Google, etc.), et
qu'il puisse un minimum gérer son profil.
\hypertarget{_gestion_des_listes}{%
\subsection{Gestion des listes}\label{_gestion_des_listes}}
\hypertarget{_moduxe8lisation}{%
\subsubsection{Modèlisation}\label{_moduxe8lisation}}
Les données suivantes doivent être associées à une liste:
\begin{itemize}
\item
un identifiant
\item
un identifiant externe (un GUID, par exemple)
\item
un nom
\item
une description
\item
le propriétaire, associé à l'utilisateur qui l'aura créée
\item
une date de création
\item
une date de modification
\end{itemize}
\hypertarget{_fonctionnalituxe9s}{%
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s}}
\begin{itemize}
\item
Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et
supprimer une liste dont il est le propriétaire
\item
Un utilisateur doit pouvoir associer ou retirer des souhaits à une
liste dont il est le propriétaire
\item
Il faut pouvoir accéder à une liste, avec un utilisateur authentifier
ou non, \textbf{via} son identifiant externe
\item
Il faut pouvoir envoyer un email avec le lien vers la liste, contenant
son identifiant externe
\item
L'utilisateur doit pouvoir voir toutes les listes qui lui
appartiennent
\end{itemize}
\hypertarget{_gestion_des_souhaits}{%
\subsection{Gestion des souhaits}\label{_gestion_des_souhaits}}
\hypertarget{_moduxe9lisation_2}{%
\subsubsection{Modélisation}\label{_moduxe9lisation_2}}
Les données suivantes peuvent être associées à un souhait:
\begin{itemize}
\item
un identifiant
\item
identifiant de la liste
\item
un nom
\item
une description
\item
le propriétaire
\item
une date de création
\item
une date de modification
\item
une image, afin de représenter l'objet ou l'idée
\item
un nombre (1 par défaut)
\item
un prix facultatif
\item
un nombre de part, facultatif également, si un prix est fourni.
\end{itemize}
\hypertarget{_fonctionnalituxe9s_2}{%
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_2}}
\begin{itemize}
\item
Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et
supprimer un souhait dont il est le propriétaire.
\item
On ne peut créer un souhait sans liste associée
\item
Il faut pouvoir fractionner un souhait uniquement si un prix est
donné.
\item
Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié
ou non.
\item
Il faut pouvoir réaliser un souhait ou une partie seulement, avec un
utilisateur authentifié ou non.
\item
Un souhait en cours de réalisation et composé de différentes parts ne
peut plus être modifié.
\item
Un souhait en cours de réalisation ou réalisé ne peut plus être
supprimé.
\item
On peut modifier le nombre de fois qu'un souhait doit être réalisé
dans la limite des réalisations déjà effectuées.
\end{itemize}
\hypertarget{_gestion_des_ruxe9alisations_de_souhaits}{%
\subsection{Gestion des réalisations de
souhaits}\label{_gestion_des_ruxe9alisations_de_souhaits}}
\hypertarget{_moduxe9lisation_3}{%
\subsubsection{Modélisation}\label{_moduxe9lisation_3}}
Les données suivantes peuvent être associées à une réalisation de
souhait:
\begin{itemize}
\item
identifiant du souhait
\item
identifiant de l'utilisateur si connu
\item
identifiant de la personne si utilisateur non connu
\item
un commentaire
\item
une date de réalisation
\end{itemize}
\hypertarget{_fonctionnalituxe9s_3}{%
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_3}}
\begin{itemize}
\item
L'utilisateur doit pouvoir voir si un souhait est réalisé, en partie
ou non. Il doit également avoir un pourcentage de complétion sur la
possibilité de réalisation de son souhait, entre 0\% et 100\%.
\item
L'utilisateur doit pouvoir voir la ou les personnes ayant réalisé un
souhait.
\item
Il y a autant de réalisation que de parts de souhait réalisées ou de
nombre de fois que le souhait est réalisé.
\end{itemize}
\hypertarget{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}{%
\subsection{Gestion des personnes réalisants les souhaits et qui ne sont
pas
connues}\label{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}}
\hypertarget{_moduxe9lisation_4}{%
\subsubsection{Modélisation}\label{_moduxe9lisation_4}}
Les données suivantes peuvent être associées à une personne réalisant un
souhait:
\begin{itemize}
\item
un identifiant
\item
un nom
\item
une adresse email facultative
\end{itemize}
\hypertarget{_fonctionnalituxe9s_4}{%
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_4}}
Modélisation
L'ORM de Django permet de travailler uniquement avec une définition de
classes, et de faire en sorte que le lien avec la base de données soit
géré uniquement de manière indirecte, par Django lui-même. On peut
schématiser ce comportement par une classe = une table.
Comme on l'a vu dans la description des fonctionnalités, on va
\textbf{grosso modo} avoir besoin des éléments suivants:
\begin{itemize}
\item
Des listes de souhaits
\item
Des éléments qui composent ces listes
\item
Des parts pouvant composer chacun de ces éléments
\item
Des utilisateurs pour gérer tout ceci.
\end{itemize}
Nous proposons dans un premier temps d'éluder la gestion des
utilisateurs, et de simplement se concentrer sur les fonctionnalités
principales. Cela nous donne ceci:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
# wish/models.py
\end{verbatim}
\begin{verbatim}
from django.db import models
\end{verbatim}
\begin{verbatim}
class Wishlist(models.Model):
pass
\end{verbatim}
\begin{verbatim}
class Item(models.Model):
pass
\end{verbatim}
\begin{verbatim}
class Part(models.Model):
pass
\end{verbatim}
\end{enumerate}
Les classes sont créées, mais vides. Entrons dans les détails.
Listes de souhaits
Comme déjà décrit précédemment, les listes de souhaits peuvent
s'apparenter simplement à un objet ayant un nom et une description. Pour
rappel, voici ce qui avait été défini dans les spécifications:
\begin{itemize}
\item
un identifiant
\item
un identifiant externe
\item
un nom
\item
une description
\item
une date de création
\item
une date de modification
\end{itemize}
Notre classe \texttt{Wishlist} peut être définie de la manière suivante:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
# wish/models.py
\end{verbatim}
\begin{verbatim}
class Wishlist(models.Model):
\end{verbatim}
\begin{verbatim}
name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
external_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
\end{verbatim}
\end{enumerate}
Que peut-on constater?
\begin{itemize}
\item
Que s'il n'est pas spécifié, un identifiant \texttt{id} sera
automatiquement généré et accessible dans le modèle. Si vous souhaitez
malgré tout spécifier que ce soit un champ en particulier qui devienne
la clé primaire, il suffit de l'indiquer grâce à l'attribut
\texttt{primary\_key=True}.
\item
Que chaque type de champs (\texttt{DateTimeField}, \texttt{CharField},
\texttt{UUIDField}, etc.) a ses propres paramètres d'initialisation.
Il est intéressant de les apprendre ou de se référer à la
documentation en cas de doute.
\end{itemize}
Au niveau de notre modélisation:
\begin{itemize}
\item
La propriété \texttt{created\_at} est gérée automatiquement par Django
grâce à l'attribut \texttt{auto\_now\_add}: de cette manière, lors
d'un \textbf{ajout}, une valeur par défaut ("\textbf{maintenant}")
sera attribuée à cette propriété.
\item
La propriété \texttt{updated\_at} est également gérée automatique,
cette fois grâce à l'attribut \texttt{auto\_now} initialisé à
\texttt{True}: lors d'une \textbf{mise à jour}, la propriété se verra
automatiquement assigner la valeur du moment présent. Cela ne permet
évidemment pas de gérer un historique complet et ne nous dira pas
\textbf{quels champs} ont été modifiés, mais cela nous conviendra dans
un premier temps.
\item
La propriété \texttt{external\_id} est de type \texttt{UUIDField}.
Lorsqu'une nouvelle instance sera instanciée, cette propriété prendra
la valeur générée par la fonction \texttt{uuid.uuid4()}. \textbf{A
priori}, chacun des types de champs possède une propriété
\texttt{default}, qui permet d'initialiser une valeur sur une nouvelle
instance.
\end{itemize}
Souhaits
Nos souhaits ont besoin des propriétés suivantes:
\begin{itemize}
\item
un identifiant
\item
l'identifiant de la liste auquel le souhait est lié
\item
un nom
\item
une description
\item
le propriétaire
\item
une date de création
\item
une date de modification
\item
une image permettant de le représenter.
\item
un nombre (1 par défaut)
\item
un prix facultatif
\item
un nombre de part facultatif, si un prix est fourni.
\end{itemize}
Après implémentation, cela ressemble à ceci:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
# wish/models.py
\end{verbatim}
\begin{verbatim}
class Wish(models.Model):
\end{verbatim}
\begin{verbatim}
wishlist = models.ForeignKey(Wishlist)
name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
picture = models.ImageField()
numbers_available = models.IntegerField(default=1)
number_of_parts = models.IntegerField(null=True)
estimated_price = models.DecimalField(max_digits=19, decimal_places=2,
null=True)
\end{verbatim}
\end{enumerate}
A nouveau, que peut-on constater ?
\begin{itemize}
\item
Les clés étrangères sont gérées directement dans la déclaration du
modèle. Un champ de type `ForeignKey
\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/models/fields/\#django.db.models.ForeignKey\%3E\%60_}
permet de déclarer une relation 1-N entre deux classes. Dans la même
veine, une relation 1-1 sera représentée par un champ de type
`OneToOneField
\textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/\%3E\%60}\emph{,
alors qu'une relation N-N utilisera un `ManyToManyField
\textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/\%3E\%60}}.
\item
L'attribut \texttt{default} permet de spécifier une valeur initiale,
utilisée lors de la construction de l'instance. Cet attribut peut
également être une fonction.
\item
Pour rendre un champ optionnel, il suffit de lui ajouter l'attribut
\texttt{null=True}.
\item
Comme cité ci-dessus, chaque champ possède des attributs spécifiques.
Le champ \texttt{DecimalField} possède par exemple les attributs
\texttt{max\_digits} et \texttt{decimal\_places}, qui nous permettra
de représenter une valeur comprise entre 0 et plus d'un milliard (avec
deux chiffres décimaux).
\item
L'ajout d'un champ de type \texttt{ImageField} nécessite
l'installation de \texttt{pillow} pour la gestion des images. Nous
l'ajoutons donc à nos pré-requis, dans le fichier
\texttt{requirements/base.txt}.
\end{itemize}
Parts
Les parts ont besoins des propriétés suivantes:
\begin{itemize}
\item
un identifiant
\item
identifiant du souhait
\item
identifiant de l'utilisateur si connu
\item
identifiant de la personne si utilisateur non connu
\item
un commentaire
\item
une date de réalisation
\end{itemize}
Elles constituent la dernière étape de notre modélisation et représente
la réalisation d'un souhait. Il y aura autant de part d'un souhait que
le nombre de souhait à réaliser fois le nombre de part.
Elles permettent à un utilisateur de participer au souhait émis par un
autre utilisateur. Pour les modéliser, une part est liée d'un côté à un
souhait, et d'autre part à un utilisateur. Cela nous donne ceci:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
from django.contrib.auth.models import User
\end{verbatim}
\begin{verbatim}
class WishPart(models.Model):
\end{verbatim}
\begin{verbatim}
wish = models.ForeignKey(Wish)
user = models.ForeignKey(User, null=True)
unknown_user = models.ForeignKey(UnknownUser, null=True)
comment = models.TextField(null=True, blank=True)
done_at = models.DateTimeField(auto_now_add=True)
\end{verbatim}
\end{enumerate}
La classe \texttt{User} référencée au début du snippet correspond à
l'utilisateur qui sera connecté. Ceci est géré par Django. Lorsqu'une
requête est effectuée et est transmise au serveur, cette information
sera disponible grâce à l'objet \texttt{request.user}, transmis à chaque
fonction ou \textbf{Class-based-view}. C'est un des avantages d'un
framework tout intégré: il vient \textbf{batteries-included} et beaucoup
de détails ne doivent pas être pris en compte. Pour le moment, nous nous
limiterons à ceci. Par la suite, nous verrons comment améliorer la
gestion des profils utilisateurs, comment y ajouter des informations et
comment gérer les cas particuliers.
La classe \texttt{UnknownUser} permet de représenter un utilisateur non
enregistré sur le site et est définie au point suivant.
Utilisateurs inconnus
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
todo:: je supprimerais pour que tous les utilisateurs soient gérés au
même endroit.
\end{enumerate}
Pour chaque réalisation d'un souhait par quelqu'un, il est nécessaire de
sauver les données suivantes, même si l'utilisateur n'est pas enregistré
sur le site:
\begin{itemize}
\item
un identifiant
\item
un nom
\item
une adresse email. Cette adresse email sera unique dans notre base de
données, pour ne pas créer une nouvelle occurence si un même
utilisateur participe à la réalisation de plusieurs souhaits.
\end{itemize}
Ceci nous donne après implémentation:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
class UnkownUser(models.Model):
\end{verbatim}
\begin{verbatim}
name = models.CharField(max_length=255)
email = models.CharField(email = models.CharField(max_length=255, unique=True)
\end{verbatim}
\end{enumerate}
\hypertarget{_tests_unitaires_2}{%
\section{Tests unitaires}\label{_tests_unitaires_2}}
\hypertarget{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}{%
\subsection{Pourquoi s'ennuyer à écrire des
tests?}\label{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}}
Traduit grossièrement depuis un article sur `https://medium.com
\textless{}\url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_}:
\begin{verbatim}
Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont
\end{verbatim}
\begin{verbatim}
Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:
\end{verbatim}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Aide au design: écrire des tests avant d'écrire le code vous donnera
une meilleure perspective sur le design à appliquer aux API.
\item
Documentation (pour les développeurs): chaque description d'un test
\item
Tester votre compréhension en tant que développeur:
\item
Assurance qualité: des tests, 5.
\end{enumerate}
\hypertarget{_why_bother_with_test_discipline}{%
\subsection{Why Bother with Test
Discipline?}\label{_why_bother_with_test_discipline}}
Your tests are your first and best line of defense against software
defects. Your tests are more important than linting \& static analysis
(which can only find a subclass of errors, not problems with your actual
program logic). Tests are as important as the implementation itself (all
that matters is that the code meets the requirement---how it's
implemented doesn't matter at all unless it's implemented poorly).
Unit tests combine many features that make them your secret weapon to
application success:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Design aid: Writing tests first gives you a clearer perspective on the
ideal API design.
\item
Feature documentation (for developers): Test descriptions enshrine in
code every implemented feature requirement.
\item
Test your developer understanding: Does the developer understand the
problem enough to articulate in code all critical component
requirements?
\item
Quality Assurance: Manual QA is error prone. In my experience, it's
impossible for a developer to remember all features that need testing
after making a change to refactor, add new features, or remove
features.
\item
Continuous Delivery Aid: Automated QA affords the opportunity to
automatically prevent broken builds from being deployed to production.
\end{enumerate}
Unit tests don't need to be twisted or manipulated to serve all of those
broad-ranging goals. Rather, it is in the essential nature of a unit
test to satisfy all of those needs. These benefits are all side-effects
of a well-written test suite with good coverage.
\hypertarget{_what_are_you_testing}{%
\subsection{What are you testing?}\label{_what_are_you_testing}}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
What component aspect are you testing?
\item
What should the feature do? What specific behavior requirement are you
testing?
\end{enumerate}
\hypertarget{_couverture_de_code_2}{%
\subsection{Couverture de code}\label{_couverture_de_code_2}}
On a vu au chapitre 1 qu'il était possible d'obtenir une couverture de
code, c'est-à-dire un pourcentage.
\hypertarget{_comment_tester}{%
\subsection{Comment tester ?}\label{_comment_tester}}
Il y a deux manières d'écrire les tests: soit avant, soit après
l'implémentation. Oui, idéalement, les tests doivent être écrits à
l'avance. Entre nous, on ne va pas râler si vous faites l'inverse,
l'important étant que vous le fassiez. Une bonne métrique pour vérifier
l'avancement des tests est la couverture de code.
Pour l'exemple, nous allons écrire la fonction
\texttt{percentage\_of\_completion} sur la classe \texttt{Wish}, et nous
allons spécifier les résultats attendus avant même d'implémenter son
contenu. Prenons le cas où nous écrivons la méthode avant son test:
\begin{Shaded}
\begin{Highlighting}[]
\KeywordTok{class}\NormalTok{ Wish(models.Model):}
\NormalTok{ [...]}
\AttributeTok{@property}
\KeywordTok{def}\NormalTok{ percentage\_of\_completion(}\VariableTok{self}\NormalTok{):}
\CommentTok{"""}
\CommentTok{ Calcule le pourcentage de complétion pour un élément.}
\CommentTok{ """}
\NormalTok{ number\_of\_linked\_parts }\OperatorTok{=}\NormalTok{ WishPart.objects.}\BuiltInTok{filter}\NormalTok{(wish}\OperatorTok{=}\VariableTok{self}\NormalTok{).count()}
\NormalTok{ total }\OperatorTok{=} \VariableTok{self}\NormalTok{.number\_of\_parts }\OperatorTok{*} \VariableTok{self}\NormalTok{.numbers\_available}
\NormalTok{ percentage }\OperatorTok{=}\NormalTok{ (number\_of\_linked\_parts }\OperatorTok{/}\NormalTok{ total)}
\ControlFlowTok{return}\NormalTok{ percentage }\OperatorTok{*} \DecValTok{100}
\end{Highlighting}
\end{Shaded}
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
\begin{verbatim}
$ coverage run --source "." src/manage.py test wish
$ coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 36 5 0 0 88%
------------------------------------------------------------------
TOTAL 69 5 4 1 93%
\end{verbatim}
Si vous générez le rapport HTML avec la commande \texttt{coverage\ html}
et que vous ouvrez le fichier
\texttt{coverage\_html\_report/src\_wish\_models\_py.html}, vous verrez
que les méthodes en rouge ne sont pas testées. \textbf{A contrario}, la
couverture de code atteignait \textbf{98\%} avant l'ajout de cette
nouvelle méthode.
Pour cela, on va utiliser un fichier \texttt{tests.py} dans notre
application \texttt{wish}. \textbf{A priori}, ce fichier est créé
automatiquement lorsque vous initialisez une nouvelle application.
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
\KeywordTok{class}\NormalTok{ TestWishModel(TestCase):}
\KeywordTok{def}\NormalTok{ test\_percentage\_of\_completion(}\VariableTok{self}\NormalTok{):}
\CommentTok{"""}
\CommentTok{ Vérifie que le pourcentage de complétion d\textquotesingle{}un souhait}
\CommentTok{ est correctement calculé.}
\CommentTok{ Sur base d\textquotesingle{}un souhait, on crée quatre parts et on vérifie}
\CommentTok{ que les valeurs s\textquotesingle{}étalent correctement sur 25\%, 50\%, 75\% et 100\%.}
\CommentTok{ """}
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist(name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake WishList\textquotesingle{}}\NormalTok{,}
\NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wishlist\textquotesingle{}}\NormalTok{)}
\NormalTok{ wishlist.save()}
\NormalTok{ wish }\OperatorTok{=}\NormalTok{ Wish(wishlist}\OperatorTok{=}\NormalTok{wishlist,}
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake Wish\textquotesingle{}}\NormalTok{,}
\NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wish\textquotesingle{}}\NormalTok{,}
\NormalTok{ number\_of\_parts}\OperatorTok{=}\DecValTok{4}\NormalTok{)}
\NormalTok{ wish.save()}
\NormalTok{ part1 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part1\textquotesingle{}}\NormalTok{)}
\NormalTok{ part1.save()}
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{25}\NormalTok{, wish.percentage\_of\_completion)}
\NormalTok{ part2 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part2\textquotesingle{}}\NormalTok{)}
\NormalTok{ part2.save()}
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{50}\NormalTok{, wish.percentage\_of\_completion)}
\NormalTok{ part3 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part3\textquotesingle{}}\NormalTok{)}
\NormalTok{ part3.save()}
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{75}\NormalTok{, wish.percentage\_of\_completion)}
\NormalTok{ part4 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part4\textquotesingle{}}\NormalTok{)}
\NormalTok{ part4.save()}
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{100}\NormalTok{, wish.percentage\_of\_completion)}
\end{Highlighting}
\end{Shaded}
L'attribut \texttt{@property} sur la méthode
\texttt{percentage\_of\_completion()} va nous permettre d'appeler
directement la méthode \texttt{percentage\_of\_completion()} comme s'il
s'agissait d'une propriété de la classe, au même titre que les champs
\texttt{number\_of\_parts} ou \texttt{numbers\_available}. Attention que
ce type de méthode contactera la base de données à chaque fois qu'elle
sera appelée. Il convient de ne pas surcharger ces méthodes de
connexions à la base: sur de petites applications, ce type de
comportement a très peu d'impacts, mais ce n'est plus le cas sur de
grosses applications ou sur des méthodes fréquemment appelées. Il
convient alors de passer par un mécanisme de \textbf{cache}, que nous
aborderons plus loin.
En relançant la couverture de code, on voit à présent que nous arrivons
à 99\%:
\begin{verbatim}
$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 34 0 0 0 100%
src\wish\tests.py 20 0 0 0 100%
------------------------------------------------------------------
TOTAL 87 0 4 1 99%
\end{verbatim}
En continuant de cette manière (ie. Ecriture du code et des tests,
vérification de la couverture de code), on se fixe un objectif idéal dès
le début du projet. En prenant un développement en cours de route,
fixez-vous comme objectif de ne jamais faire baisser la couverture de
code.
\hypertarget{_quelques_liens_utiles}{%
\subsection{Quelques liens utiles}\label{_quelques_liens_utiles}}
\begin{itemize}
\item
`Django factory boy
\textless{}\url{https://github.com/rbarrois/django-factory_boy/tree/v1.0.0\%3E\%60_}
\end{itemize}
\hypertarget{_refactoring}{%
\section{Refactoring}\label{_refactoring}}
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. Django gère trois sortes d'héritage:
\begin{itemize}
\item
L'héritage par classe abstraite
\item
L'héritage classique
\item
L'héritage par classe proxy.
\end{itemize}
\hypertarget{_classe_abstraite}{%
\subsection{Classe abstraite}\label{_classe_abstraite}}
L'héritage par classe abstraite consiste à déterminer une classe mère
qui ne sera jamais instanciée. C'est utile pour définir des champs qui
se répèteront dans plusieurs autres classes et surtout pour respecter le
principe de DRY. Comme la classe mère ne sera jamais instanciée, ces
champs seront en fait dupliqués physiquement, et traduits en SQL, dans
chacune des classes filles.
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# wish/models.py}
\KeywordTok{class}\NormalTok{ AbstractModel(models.Model):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ abstract }\OperatorTok{=} \VariableTok{True}
\NormalTok{ created\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now\_add}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
\NormalTok{ updated\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Wishlist(AbstractModel):}
\ControlFlowTok{pass}
\KeywordTok{class}\NormalTok{ Item(AbstractModel):}
\ControlFlowTok{pass}
\KeywordTok{class}\NormalTok{ Part(AbstractModel):}
\ControlFlowTok{pass}
\end{Highlighting}
\end{Shaded}
En traduisant ceci en SQL, on aura en fait trois tables, chacune
reprenant les champs \texttt{created\_at} et \texttt{updated\_at}, ainsi
que son propre identifiant:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{{-}{-}$ python manage.py sql wish}
\ControlFlowTok{BEGIN}\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{COMMIT}\NormalTok{;}
\end{Highlighting}
\end{Shaded}
\hypertarget{_huxe9ritage_classique}{%
\subsection{Héritage classique}\label{_huxe9ritage_classique}}
L'héritage classique est généralement déconseillé, car il peut
introduire très rapidement un problème de performances: en reprenant
l'exemple introduit avec l'héritage par classe abstraite, et en omettant
l'attribut \texttt{abstract\ =\ True}, on se retrouvera en fait avec
quatre tables SQL:
\begin{itemize}
\item
Une table \texttt{AbstractModel}, qui reprend les deux champs
\texttt{created\_at} et \texttt{updated\_at}
\item
Une table \texttt{Wishlist}
\item
Une table \texttt{Item}
\item
Une table \texttt{Part}.
\end{itemize}
A nouveau, en analysant la sortie SQL de cette modélisation, on obtient
ceci:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{{-}{-}$ python manage.py sql wish}
\ControlFlowTok{BEGIN}\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (}
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (}
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (}
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{COMMIT}\NormalTok{;}
\end{Highlighting}
\end{Shaded}
Le problème est que les identifiants seront définis et incrémentés au
niveau de la table mère. Pour obtenir les informations héritées, nous
seront obligés de faire une jointure. En gros, impossible d'obtenir les
données complètes pour l'une des classes de notre travail de base sans
effectuer un \textbf{join} sur la classe mère.
Dans ce sens, cela va encore\ldots\hspace{0pt} Mais imaginez que vous
définissiez une classe \texttt{Wishlist}, de laquelle héritent les
classes \texttt{ChristmasWishlist} et \texttt{EasterWishlist}: pour
obtenir la liste complètes des listes de souhaits, il vous faudra faire
une jointure \textbf{externe} sur chacune des tables possibles, avant
même d'avoir commencé à remplir vos données. Il est parfois nécessaire
de passer par cette modélisation, mais en étant conscient des risques
inhérents.
\hypertarget{_classe_proxy}{%
\subsection{Classe proxy}\label{_classe_proxy}}
Lorsqu'on définit une classe de type \textbf{proxy}, on fait en sorte
que cette nouvelle classe ne définisse aucun nouveau champ sur la classe
mère. Cela ne change dès lors rien à la traduction du modèle de données
en SQL, puisque la classe mère sera traduite par une table, et la classe
fille ira récupérer les mêmes informations dans la même table: elle ne
fera qu'ajouter ou modifier un comportement dynamiquement, sans ajouter
d'emplacements de stockage supplémentaires.
Nous pourrions ainsi définir les classes suivantes:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# wish/models.py}
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ description }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{2000}\NormalTok{)}
\NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ models.DateField()}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description, expiration\_date}\OperatorTok{=}\VariableTok{None}\NormalTok{):}
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist()}
\NormalTok{ wishlist.name }\OperatorTok{=}\NormalTok{ name}
\NormalTok{ wishlist.description }\OperatorTok{=}\NormalTok{ description}
\NormalTok{ wishlist.expiration\_date }\OperatorTok{=}\NormalTok{ expiration\_date}
\NormalTok{ wishlist.save()}
\ControlFlowTok{return}\NormalTok{ wishlist}
\KeywordTok{class}\NormalTok{ ChristmasWishlist(Wishlist):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ proxy }\OperatorTok{=} \VariableTok{True}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):}
\NormalTok{ christmas }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{12}\NormalTok{, }\DecValTok{31}\NormalTok{)}
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, christmas)}
\NormalTok{ w.save()}
\KeywordTok{class}\NormalTok{ EasterWishlist(Wishlist):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ proxy }\OperatorTok{=} \VariableTok{True}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):}
\NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{4}\NormalTok{, }\DecValTok{1}\NormalTok{)}
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, expiration\_date)}
\NormalTok{ w.save()}
\end{Highlighting}
\end{Shaded}
Gestion des utilisateurs
Dans les spécifications, nous souhaitions pouvoir associer un
utilisateur à une liste (\textbf{le propriétaire}) et un utilisateur à
une part (\textbf{le donateur}). Par défaut, Django offre une gestion
simplifiée des utilisateurs (pas de connexion LDAP, pas de double
authentification, \ldots\hspace{0pt}): juste un utilisateur et un mot de
passe. Pour y accéder, un paramètre par défaut est défini dans votre
fichier de settings: \texttt{AUTH\_USER\_MODEL}.
\hypertarget{_khana}{%
\section{Khana}\label{_khana}}
Khana est une application de suivi d'apprentissage pour des élèves ou
étudiants. Nous voulons pouvoir:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Lister les élèves
\item
Faire des listes de présence pour les élèves
\item
Pouvoir planifier ses cours
\item
Pouvoir suivre l'apprentissage des élèves, les liens qu'ils ont entre
les éléments à apprendre:
\item
pour écrire une phrase, il faut pouvoir écrire des mots, connaître la
grammaire, et connaître la conjugaison
\item
pour écrire des mots, il faut savoir écrire des lettres
\item
\ldots\hspace{0pt}
\end{enumerate}
Plusieurs professeurs s'occupent d'une même classe; il faut pouvoir
écrire des notes, envoyer des messages aux autres professeurs, etc.
Il faut également pouvoir définir des dates de contrôle, voir combien de
semaines il reste pour s'assurer d'avoir vu toute la matiètre.
Et pouvoir encoder les points des contrôles.
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-khana.png}
\caption{Khana}
\end{figure}
Unresolved directive in part-5-go-live/\_index.adoc -
include::legacy/\_main.adoc{[}{]}
\hypertarget{_glossaire}{%
\section{Glossaire}\label{_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}
\hypertarget{_bibliographie}{%
\section{Bibliographie}\label{_bibliographie}}
bibliography::{[}{]}
\end{document}