From b65a23c66b211b7533c350a39b6e12c89957e9bd Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Wed, 23 Mar 2022 09:06:22 +0100 Subject: [PATCH] Add automatic LaTeX conversion --- source/main.tex | 10719 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 10719 insertions(+) create mode 100644 source/main.tex diff --git a/source/main.tex b/source/main.tex new file mode 100644 index 0000000..2ef1995 --- /dev/null +++ b/source/main.tex @@ -0,0 +1,10719 @@ +% 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 l’on résumera ainsi: « don’t depend on things you don’t need » 😘 +Au niveau des composants, au niveau architectural, mais également à d’autres 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{Don’t\ 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{}don’t\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ qu’une\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ l’attribut\ \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\ l’application\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 ; + 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 +| +\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}