10720 lines
456 KiB
TeX
10720 lines
456 KiB
TeX
% 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 <server_name>;
|
||
root /var/www/gwift;
|
||
error_log /var/log/nginx/gwift_error.log;
|
||
access_log /var/log/nginx/gwift_access.log;
|
||
|
||
client_max_body_size 4G;
|
||
keepalive_timeout 5;
|
||
|
||
gzip on;
|
||
gzip_comp_level 7;
|
||
gzip_proxied any;
|
||
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
|
||
|
||
|
||
location /static/ {
|
||
access_log off;
|
||
expires 30d;
|
||
add_header Pragma public;
|
||
add_header Cache-Control "public";
|
||
add_header Vary "Accept-Encoding";
|
||
try_files $uri $uri/ =404;
|
||
}
|
||
|
||
location / {
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header Host $http_host;
|
||
proxy_redirect off;
|
||
|
||
proxy_pass http://gwift_app;
|
||
}
|
||
}
|
||
\end{verbatim}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Ce répertoire sera complété par la commande \texttt{collectstatic} que
|
||
l'on verra plus tard. L'objectif est que les fichiers ne demandant
|
||
aucune intelligence soit directement servis par Nginx. Cela évite
|
||
d'avoir un processus Python (relativement lent) qui doive être
|
||
instancié pour servir un simple fichier statique.
|
||
\item
|
||
Afin d'éviter que Django ne reçoive uniquement des requêtes provenant
|
||
de 127.0.0.1
|
||
\end{itemize}
|
||
|
||
\hypertarget{_mise_uxe0_jour}{%
|
||
\subsection{Mise à jour}\label{_mise_uxe0_jour}}
|
||
|
||
Script de mise à jour.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\FunctionTok{su}\NormalTok{ {-} }\OperatorTok{\textless{}}\NormalTok{user}\OperatorTok{\textgreater{}}
|
||
\BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}}\NormalTok{/bin/activate}
|
||
\BuiltInTok{cd}\NormalTok{ \textasciitilde{}/webapps/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}}
|
||
\FunctionTok{git}\NormalTok{ fetch}
|
||
\FunctionTok{git}\NormalTok{ checkout vX.Y.Z}
|
||
\ExtensionTok{pip}\NormalTok{ install {-}U requirements/prod.txt}
|
||
\ExtensionTok{python}\NormalTok{ manage.py migrate}
|
||
\ExtensionTok{python}\NormalTok{ manage.py collectstatic}
|
||
\BuiltInTok{kill}\NormalTok{ {-}HUP }\KeywordTok{\textasciigrave{}}\FunctionTok{ps}\NormalTok{ {-}C gunicorn fch {-}o pid }\KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}n 1}\KeywordTok{\textasciigrave{}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file}
|
||
\end{itemize}
|
||
|
||
\hypertarget{_configuration_des_sauvegardes}{%
|
||
\subsection{Configuration des
|
||
sauvegardes}\label{_configuration_des_sauvegardes}}
|
||
|
||
Les sauvegardes ont été configurées avec borg:
|
||
\texttt{yum\ install\ borgbackup}.
|
||
|
||
C'est l'utilisateur gwift qui s'en occupe.
|
||
|
||
\begin{verbatim}
|
||
mkdir -p /home/gwift/borg-backups/
|
||
cd /home/gwift/borg-backups/
|
||
borg init gwift.borg -e=none
|
||
borg create gwift.borg::{now} ~/bin ~/webapps
|
||
\end{verbatim}
|
||
|
||
Et dans le fichier crontab :
|
||
|
||
\begin{verbatim}
|
||
0 23 * * * /home/gwift/bin/backup.sh
|
||
\end{verbatim}
|
||
|
||
\hypertarget{_rotation_des_jounaux}{%
|
||
\subsection{Rotation des jounaux}\label{_rotation_des_jounaux}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ExtensionTok{/var/log/gwift/*}\NormalTok{ \{}
|
||
\ExtensionTok{weekly}
|
||
\ExtensionTok{rotate}\NormalTok{ 3}
|
||
\FunctionTok{size}\NormalTok{ 10M}
|
||
\ExtensionTok{compress}
|
||
\ExtensionTok{delaycompress}
|
||
\NormalTok{\}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift
|
||
pour vérifier que cela fonctionne correctement.
|
||
|
||
\hypertarget{_ansible}{%
|
||
\subsection{Ansible}\label{_ansible}}
|
||
|
||
TODO
|
||
|
||
\hypertarget{_duxe9ploiement_sur_heroku}{%
|
||
\section{Déploiement sur Heroku}\label{_duxe9ploiement_sur_heroku}}
|
||
|
||
\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A
|
||
Service} paas, où vous choisissez le \emph{service} dont vous avez
|
||
besoin (une base de données, un service de cache, un service applicatif,
|
||
\ldots\hspace{0pt}), vous lui envoyer les paramètres nécessaires et le
|
||
tout démarre gentiment sans que vous ne deviez superviser l'hôte. Ce
|
||
mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà
|
||
parlé plus tôt - raison de plus pour que notre application soit
|
||
directement prête à y être déployée, d'autant plus qu'il ne sera pas
|
||
possible de modifier un fichier une fois qu'elle aura démarré: si vous
|
||
souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et
|
||
envoyer de nouveaux paramètres et recommencer le déploiement depuis le
|
||
début.
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/deployment/heroku.png}
|
||
\caption{Invest in apps, not ops. Heroku handles the hard stuff ---
|
||
patching and upgrading, 24/7 ops and security, build systems, failovers,
|
||
and more --- so your developers can stay focused on building great
|
||
apps.}
|
||
\end{figure}
|
||
|
||
Pour un projet de type "hobby" et pour l'exemple de déploiement
|
||
ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un
|
||
kopek, afin de tester nos quelques idées ou mettre rapidement un
|
||
\emph{Most Valuable Product} en place. La seule contrainte consistera à
|
||
pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra
|
||
être fait en configurant un \emph{bucket compatible S3}, par exemple
|
||
chez Amazon, Scaleway ou OVH.
|
||
|
||
Le fonctionnement est relativement simple: pour chaque application,
|
||
Heroku crée un dépôt Git qui lui est associé. Il suffit donc d'envoyer
|
||
les sources de votre application vers ce dépôt pour qu'Heroku les
|
||
interprête comme étant une nouvelle version, déploie les nouvelles
|
||
fonctionnalités - sous réserve que tous les tests passent correctement -
|
||
et les mettent à disposition. Dans un fonctionnement plutôt manuel,
|
||
chaque déploiement est initialisé par le développeur ou par un membre de
|
||
l'équipe. Dans une version plus automatisée, chacun de ces déploiements
|
||
peut être placé en fin de \emph{pipeline}, lorsque tous les tests
|
||
unitaires et d'intégration auront été réalisés.
|
||
|
||
Au travers de la commande \texttt{heroku\ create}, vous associez donc
|
||
une nouvelle référence à votre code source, comme le montre le contenu
|
||
du fichier \texttt{.git/config} ci-dessous:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create}
|
||
\ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098}
|
||
\ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git}
|
||
|
||
\NormalTok{$ }\FunctionTok{cat}\NormalTok{ .git/config}
|
||
\NormalTok{[}\ExtensionTok{core}\NormalTok{]}
|
||
\ExtensionTok{repositoryformatversion}\NormalTok{ = 0}
|
||
\ExtensionTok{filemode}\NormalTok{ = false}
|
||
\ExtensionTok{bare}\NormalTok{ = false}
|
||
\ExtensionTok{logallrefupdates}\NormalTok{ = true}
|
||
\ExtensionTok{symlinks}\NormalTok{ = false}
|
||
\ExtensionTok{ignorecase}\NormalTok{ = true}
|
||
\NormalTok{[}\ExtensionTok{remote} \StringTok{"heroku"}\NormalTok{]}
|
||
\ExtensionTok{url}\NormalTok{ = https://git.heroku.com/still{-}thicket{-}66406.git}
|
||
\ExtensionTok{fetch}\NormalTok{ = +refs/heads/*:refs/remotes/heroku/*}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
IMPORTANT:
|
||
|
||
\begin{verbatim}
|
||
Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration.
|
||
Celle-ci se limite aux deux fichiers suivants:
|
||
|
||
* Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI
|
||
* Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`)
|
||
\end{verbatim}
|
||
|
||
Après ce paramétrage, il suffit de pousser les changements vers ce
|
||
nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}.
|
||
|
||
Heroku propose des espaces de déploiements, mais pas d'espace de
|
||
stockage. Il est possible d'y envoyer des fichiers utilisateurs
|
||
(typiquement, des media personnalisés), mais ceux-ci seront perdus lors
|
||
du redémarrage du container. Il est donc primordial de configurer
|
||
correctement l'hébergement des fichiers média, de préférences sur un
|
||
stockage compatible S3. s3
|
||
|
||
Prêt à vous lancer ? Commencez par créer un compte:
|
||
\url{https://signup.heroku.com/python}.
|
||
|
||
\hypertarget{_configuration_du_compte_heroku}{%
|
||
\subsection{Configuration du compte
|
||
Heroku}\label{_configuration_du_compte_heroku}}
|
||
|
||
+ Récupération des valeurs d'environnement pour les réutiliser
|
||
ci-dessous.
|
||
|
||
Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre
|
||
première application; heureusement, la documentation est super bien
|
||
faite:
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/deployment/heroku-new-app.png}
|
||
\caption{Heroku: Commencer à travailler avec un langage}
|
||
\end{figure}
|
||
|
||
Installez ensuite la CLI (\emph{Command Line Interface}) en suivant
|
||
\href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation
|
||
suivante}.
|
||
|
||
Au besoin, cette CLI existe pour:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
macOS, \emph{via} `brew `
|
||
\item
|
||
Windows, grâce à un
|
||
\href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la
|
||
version 32 bits existe aussi, mais il est peu probable que vous en
|
||
ayez besoin)
|
||
\item
|
||
GNU/Linux, via un script Shell
|
||
\texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh}
|
||
ou sur \href{https://snapcraft.io/heroku}{SnapCraft}.
|
||
\end{enumerate}
|
||
|
||
Une fois installée, connectez-vous:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ login}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Et créer votre application:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create}
|
||
\ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098}
|
||
\ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/deployment/heroku-app-created.png}
|
||
\caption{Notre application est à présent configurée!}
|
||
\end{figure}
|
||
|
||
Ajoutons lui une base de données, que nous sauvegarderons à intervalle
|
||
régulier:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}postgresql:hobby{-}dev}
|
||
\ExtensionTok{Creating}\NormalTok{ heroku{-}postgresql:hobby{-}dev on ⬢ still{-}thicket{-}66406... free}
|
||
\ExtensionTok{Database}\NormalTok{ has been created and is available}
|
||
\NormalTok{ ! }\ExtensionTok{This}\NormalTok{ database is empty. If upgrading, you can transfer}
|
||
\NormalTok{ ! }\ExtensionTok{data}\NormalTok{ from another database with pg:copy}
|
||
\ExtensionTok{Created}\NormalTok{ postgresql{-}clear{-}39693 as DATABASE\_URL}
|
||
\ExtensionTok{Use}\NormalTok{ heroku addons:docs heroku{-}postgresql to view documentation}
|
||
|
||
\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ pg:backups schedule {-}{-}at }\StringTok{\textquotesingle{}14:00 Europe/Brussels\textquotesingle{}}\NormalTok{ DATABASE\_URL}
|
||
\ExtensionTok{Scheduling}\NormalTok{ automatic daily backups of postgresql{-}clear{-}39693 at 14:00 Europe/Brussels... done}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
TODO: voir comment récupérer le backup de la db :-p
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# Copié/collé de https://cookiecutter{-}django.readthedocs.io/en/latest/deployment{-}on{-}heroku.html}
|
||
\ExtensionTok{heroku}\NormalTok{ create {-}{-}buildpack https://github.com/heroku/heroku{-}buildpack{-}python}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}redis:hobby{-}dev}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ addons:create mailgun:starter}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ config:set PYTHONHASHSEED=random}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ config:set WEB\_CONCURRENCY=4}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_DEBUG=False}
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SETTINGS\_MODULE=config.settings.production}
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SECRET\_KEY=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 64}\VariableTok{)}\StringTok{"}
|
||
|
||
\CommentTok{\# Generating a 32 character{-}long random string without any of the visually similar characters "IOl01":}
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ADMIN\_URL=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 4096 }\KeywordTok{|} \FunctionTok{tr}\NormalTok{ {-}dc }\StringTok{\textquotesingle{}A{-}HJ{-}NP{-}Za{-}km{-}z2{-}9\textquotesingle{}} \KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}c 32}\VariableTok{)}\StringTok{/"}
|
||
|
||
\CommentTok{\# Set this to your Heroku app url, e.g. \textquotesingle{}bionic{-}beaver{-}28392.herokuapp.com\textquotesingle{}}
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ALLOWED\_HOSTS=}
|
||
|
||
\CommentTok{\# Assign with AWS\_ACCESS\_KEY\_ID}
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_ACCESS\_KEY\_ID=}
|
||
|
||
\CommentTok{\# Assign with AWS\_SECRET\_ACCESS\_KEY}
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_SECRET\_ACCESS\_KEY=}
|
||
|
||
\CommentTok{\# Assign with AWS\_STORAGE\_BUCKET\_NAME}
|
||
\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_STORAGE\_BUCKET\_NAME=}
|
||
|
||
\FunctionTok{git}\NormalTok{ push heroku master}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ run python manage.py createsuperuser}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ run python manage.py check {-}{-}deploy}
|
||
|
||
\ExtensionTok{heroku}\NormalTok{ open}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_configuration}{%
|
||
\subsection{Configuration}\label{_configuration}}
|
||
|
||
Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les
|
||
commandes à exécuter pour que tout fonctionne correctement. Pour un
|
||
projet Django, cela comprend, à placer à la racine de votre projet:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Un fichier \texttt{requirements.txt} (qui peut éventuellement faire
|
||
appel à un autre fichier, \textbf{via} l'argument \texttt{-r})
|
||
\item
|
||
Un fichier \texttt{Procfile} ({[}sans
|
||
extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!),
|
||
qui expliquera la commande pour le protocole WSGI.
|
||
\end{enumerate}
|
||
|
||
Dans notre exemple:
|
||
|
||
\begin{verbatim}
|
||
# requirements.txt
|
||
django==3.2.8
|
||
gunicorn
|
||
boto3
|
||
django-storages
|
||
\end{verbatim}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# Procfile}
|
||
\ExtensionTok{release}\NormalTok{: python3 manage.py migrate}
|
||
\ExtensionTok{web}\NormalTok{: gunicorn gwift.wsgi}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_huxe9bergement_s3}{%
|
||
\subsection{Hébergement S3}\label{_huxe9bergement_s3}}
|
||
|
||
Pour cette partie, nous allons nous baser sur
|
||
l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de
|
||
Scaleway}. Ils offrent 75GB de stockage et de transfert par mois, ce qui
|
||
va nous laisser suffisament d'espace pour jouer un peu 😉.
|
||
|
||
\includegraphics{images/deployment/scaleway-object-storage-bucket.png}
|
||
|
||
L'idée est qu'au moment de la construction des fichiers statiques,
|
||
Django aille simplement les héberger sur un espace de stockage
|
||
compatible S3. La complexité va être de configurer correctement les
|
||
différents points de terminaison. Pour héberger nos fichiers sur notre
|
||
\textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes
|
||
dans l'ordre:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y
|
||
en a - \textbf{littéralement} - des dizaines.
|
||
\item
|
||
Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec
|
||
ce type de protocole
|
||
\item
|
||
Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de
|
||
faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et
|
||
Django, qui s'attend à ce qu'on lui donne un moteur de gestion
|
||
\textbf{via} la clé
|
||
{[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}).
|
||
\end{enumerate}
|
||
|
||
La première étape consiste à se rendre dans {[}la console
|
||
Scaleway{]}(\url{https://console.scaleway.com/project/credentials}),
|
||
pour gérer ses identifiants et créer un jeton.
|
||
|
||
\includegraphics{images/deployment/scaleway-api-key.png}
|
||
|
||
Selon la documentation de
|
||
\href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages},
|
||
de
|
||
\href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3}
|
||
et de
|
||
\href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway},
|
||
vous aurez besoin des clés suivantes au niveau du fichier
|
||
\texttt{settings.py}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{AWS\_ACCESS\_KEY\_ID }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}ACCESS\_KEY\_ID\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{AWS\_SECRET\_ACCESS\_KEY }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}SECRET\_ACCESS\_KEY\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{AWS\_STORAGE\_BUCKET\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_STORAGE\_BUCKET\_NAME\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{AWS\_S3\_REGION\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_S3\_REGION\_NAME\textquotesingle{}}\NormalTok{)}
|
||
|
||
\NormalTok{AWS\_DEFAULT\_ACL }\OperatorTok{=} \StringTok{\textquotesingle{}public{-}read\textquotesingle{}}
|
||
\NormalTok{AWS\_LOCATION }\OperatorTok{=} \StringTok{\textquotesingle{}static\textquotesingle{}}
|
||
\NormalTok{AWS\_S3\_SIGNATURE\_VERSION }\OperatorTok{=} \StringTok{\textquotesingle{}s3v4\textquotesingle{}}
|
||
|
||
\NormalTok{AWS\_S3\_HOST }\OperatorTok{=} \StringTok{\textquotesingle{}s3.}\SpecialCharTok{\%s}\StringTok{.scw.cloud\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_REGION\_NAME,)}
|
||
\NormalTok{AWS\_S3\_ENDPOINT\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}https://}\SpecialCharTok{\%s}\StringTok{\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_HOST, )}
|
||
|
||
\NormalTok{DEFAULT\_FILE\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3Boto3Storage\textquotesingle{}}
|
||
\NormalTok{STATICFILES\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3ManifestStaticStorage\textquotesingle{}}
|
||
|
||
\NormalTok{STATIC\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}}\SpecialCharTok{\%s}\StringTok{/}\SpecialCharTok{\%s}\StringTok{/\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_ENDPOINT\_URL, AWS\_LOCATION)}
|
||
|
||
\CommentTok{\# General optimization for faster delivery}
|
||
\NormalTok{AWS\_IS\_GZIPPED }\OperatorTok{=} \VariableTok{True}
|
||
\NormalTok{AWS\_S3\_OBJECT\_PARAMETERS }\OperatorTok{=}\NormalTok{ \{}
|
||
\StringTok{\textquotesingle{}CacheControl\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}max{-}age=86400\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{\}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Configurez-les dans la console d'administration d'Heroku:
|
||
|
||
\includegraphics{images/deployment/heroku-vars-reveal.png}
|
||
|
||
Lors de la publication, vous devriez à présent avoir la sortie suivante,
|
||
qui sera confirmée par le \textbf{bucket}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ExtensionTok{remote}\NormalTok{: {-}{-}{-}{-}{-}}\OperatorTok{\textgreater{}}\NormalTok{ $ python manage.py collectstatic {-}{-}noinput}
|
||
\ExtensionTok{remote}\NormalTok{: 128 static files copied, 156 post{-}processed.}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\includegraphics{images/deployment/gwift-cloud-s3.png}
|
||
|
||
Sources complémentaires:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
{[}How to store Django static and media files on S3 in
|
||
production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/})
|
||
\item
|
||
{[}Using Django and
|
||
Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/})
|
||
\end{itemize}
|
||
|
||
\hypertarget{_docker_compose}{%
|
||
\subsection{Docker-Compose}\label{_docker_compose}}
|
||
|
||
(c/c Ced' - 2020-01-24)
|
||
|
||
Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter
|
||
pour django.
|
||
|
||
D'abords, après avoir installer docker-compose et les dépendances sous
|
||
debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root
|
||
pour utiliser docker. Ensuite, j'ai relancé mon pc car juste relancé un
|
||
shell n'a pas suffit pour que je puisse utiliser docker avec mon compte.
|
||
|
||
Bon après c'est facile, un petit virtualenv pour cookiecutter, suivit
|
||
d'une installation du template django. Et puis j'ai suivi sans t
|
||
\url{https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html}
|
||
|
||
Alors, il télécharge les images, fait un petit update, installe les
|
||
dépendances de dev, install les requirement pip \ldots\hspace{0pt}
|
||
|
||
Du coup, ça prend vite de la place: image.png
|
||
|
||
L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque
|
||
1,5 GB d'un coup.
|
||
|
||
Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien
|
||
faire ou presque.
|
||
|
||
La partie ci-dessous a été reprise telle quelle de
|
||
\href{https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html}{la
|
||
documentation de cookie-cutter-django}.
|
||
|
||
le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt
|
||
source.
|
||
|
||
On peut aussi passer par fabric, ansible, chef ou puppet.
|
||
|
||
\hypertarget{_autres_outils}{%
|
||
\section{Autres outils}\label{_autres_outils}}
|
||
|
||
Voir aussi devpi, circus, uswgi, statsd.
|
||
|
||
See \url{https://mattsegal.dev/nginx-django-reverse-proxy-config.html}
|
||
|
||
\hypertarget{_ressources}{%
|
||
\section{Ressources}\label{_ressources}}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\url{https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/}
|
||
\item
|
||
\href{https://docs.djangoproject.com/fr/3.0/howto/deployment/}{Déploiement}.
|
||
\item
|
||
Let's Encrypt !
|
||
\end{itemize}
|
||
|
||
Nous avons fait exprès de reprendre l'acronyme d'une \emph{Services
|
||
Oriented Architecture} pour cette partie. L'objectif est de vous mettre
|
||
la puce à l'oreille quant à la finalité du développement: que
|
||
l'utilisateur soit humain, bot automatique ou client Web, l'objectif est
|
||
de fournir des applications résilientes, disponibles et accessibles.
|
||
|
||
Dans cette partie, nous aborderons les vues, la mise en forme, la mise
|
||
en page, la définition d'une interface REST, la définition d'une
|
||
interface GraphQL et le routage d'URLs.
|
||
|
||
\hypertarget{_application_programming_interface}{%
|
||
\section{Application Programming
|
||
Interface}\label{_application_programming_interface}}
|
||
|
||
\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs
|
||
Django Rest Framework
|
||
|
||
Expliquer pourquoi une API est intéressante/primordiale/la première
|
||
chose à réaliser/le cadet de nos soucis.
|
||
|
||
Voir peut-être aussi
|
||
\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/}
|
||
|
||
Au niveau du modèle, nous allons partir de quelque chose de très simple:
|
||
des personnes, des contrats, des types de contrats, et un service
|
||
d'affectation. Quelque chose comme ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# models.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ models}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ People(models.Model):}
|
||
\NormalTok{ CIVILITY\_CHOICES }\OperatorTok{=}\NormalTok{ (}
|
||
\NormalTok{ (}\StringTok{"M"}\NormalTok{, }\StringTok{"Monsieur"}\NormalTok{),}
|
||
\NormalTok{ (}\StringTok{"Mme"}\NormalTok{, }\StringTok{"Madame"}\NormalTok{),}
|
||
\NormalTok{ (}\StringTok{"Dr"}\NormalTok{, }\StringTok{"Docteur"}\NormalTok{),}
|
||
\NormalTok{ (}\StringTok{"Pr"}\NormalTok{, }\StringTok{"Professeur"}\NormalTok{),}
|
||
\NormalTok{ (}\StringTok{""}\NormalTok{, }\StringTok{""}\NormalTok{)}
|
||
\NormalTok{ )}
|
||
|
||
\NormalTok{ last\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ first\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ civility }\OperatorTok{=}\NormalTok{ models.CharField(}
|
||
\NormalTok{ max\_length}\OperatorTok{=}\DecValTok{3}\NormalTok{,}
|
||
\NormalTok{ choices}\OperatorTok{=}\NormalTok{CIVILITY\_CHOICES,}
|
||
\NormalTok{ default}\OperatorTok{=}\StringTok{""}
|
||
\NormalTok{ )}
|
||
|
||
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{return} \StringTok{"}\SpecialCharTok{\{\}}\StringTok{, }\SpecialCharTok{\{\}}\StringTok{"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}\VariableTok{self}\NormalTok{.last\_name, }\VariableTok{self}\NormalTok{.first\_name)}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Service(models.Model):}
|
||
\NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.label}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ ContractType(models.Model):}
|
||
\NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ short\_label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)}
|
||
|
||
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.short\_label}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Contract(models.Model):}
|
||
\NormalTok{ people }\OperatorTok{=}\NormalTok{ models.ForeignKey(People, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
\NormalTok{ date\_begin }\OperatorTok{=}\NormalTok{ models.DateField()}
|
||
\NormalTok{ date\_end }\OperatorTok{=}\NormalTok{ models.DateField(blank}\OperatorTok{=}\VariableTok{True}\NormalTok{, null}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
|
||
\NormalTok{ contract\_type }\OperatorTok{=}\NormalTok{ models.ForeignKey(ContractType, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
\NormalTok{ service }\OperatorTok{=}\NormalTok{ models.ForeignKey(Service, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
|
||
\KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{if} \VariableTok{self}\NormalTok{.date\_end }\KeywordTok{is} \KeywordTok{not} \VariableTok{None}\NormalTok{:}
|
||
\ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, jusqu\textquotesingle{}au }\SpecialCharTok{\{\}}\StringTok{, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
|
||
\VariableTok{self}\NormalTok{.date\_begin,}
|
||
\VariableTok{self}\NormalTok{.date\_end,}
|
||
\VariableTok{self}\NormalTok{.service,}
|
||
\VariableTok{self}\NormalTok{.contract\_type}
|
||
\NormalTok{ )}
|
||
|
||
\ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, à durée indéterminée, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}
|
||
\VariableTok{self}\NormalTok{.date\_begin,}
|
||
\VariableTok{self}\NormalTok{.service,}
|
||
\VariableTok{self}\NormalTok{.contract\_type}
|
||
\NormalTok{ )}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\includegraphics{images/rest/models.png}
|
||
|
||
\hypertarget{_configuration_2}{%
|
||
\section{Configuration}\label{_configuration_2}}
|
||
|
||
La configuration des points de terminaison de notre API est relativement
|
||
touffue. Il convient de:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Configurer les sérialiseurs, càd. les champs que nous souhaitons
|
||
exposer au travers de l'API,
|
||
\item
|
||
Configurer les vues, càd le comportement de chacun des points de
|
||
terminaison,
|
||
\item
|
||
Configurer les points de terminaison eux-mêmes, càd les URLs
|
||
permettant d'accéder aux ressources.
|
||
\item
|
||
Et finalement ajouter quelques paramètres au niveau de notre
|
||
application.
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_suxe9rialiseurs}{%
|
||
\subsection{Sérialiseurs}\label{_suxe9rialiseurs}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# serializers.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group}
|
||
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ serializers}
|
||
|
||
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ People}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ ServiceSerializer(serializers.HyperlinkedModelSerializer):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Service}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"name"}\NormalTok{,)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_vues}{%
|
||
\subsection{Vues}\label{_vues}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# views.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group}
|
||
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets}
|
||
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ permissions}
|
||
|
||
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service}
|
||
\ImportTok{from}\NormalTok{ .serializers }\ImportTok{import}\NormalTok{ PeopleSerializer, ContractSerializer, ServiceSerializer}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):}
|
||
\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ People.objects.}\BuiltInTok{all}\NormalTok{()}
|
||
\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ PeopleSerializer}
|
||
\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ ContractViewSet(viewsets.ModelViewSet):}
|
||
\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Contract.objects.}\BuiltInTok{all}\NormalTok{()}
|
||
\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ContractSerializer}
|
||
\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ ServiceViewSet(viewsets.ModelViewSet):}
|
||
\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Service.objects.}\BuiltInTok{all}\NormalTok{()}
|
||
\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ServiceSerializer}
|
||
\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_urls}{%
|
||
\subsection{URLs}\label{_urls}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# urls.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
|
||
\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path, include}
|
||
|
||
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ routers}
|
||
|
||
\ImportTok{from}\NormalTok{ core }\ImportTok{import}\NormalTok{ views}
|
||
|
||
|
||
\NormalTok{router }\OperatorTok{=}\NormalTok{ routers.DefaultRouter()}
|
||
\NormalTok{router.register(}\VerbatimStringTok{r"people"}\NormalTok{, views.PeopleViewSet)}
|
||
\NormalTok{router.register(}\VerbatimStringTok{r"contracts"}\NormalTok{, views.ContractViewSet)}
|
||
\NormalTok{router.register(}\VerbatimStringTok{r"services"}\NormalTok{, views.ServiceViewSet)}
|
||
|
||
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ path(}\StringTok{"api/v1/"}\NormalTok{, include(router.urls)),}
|
||
\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls),}
|
||
\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_paramuxe8tres}{%
|
||
\subsection{Paramètres}\label{_paramuxe8tres}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# settings.py}
|
||
|
||
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ ...}
|
||
\StringTok{"rest\_framework"}\NormalTok{,}
|
||
\NormalTok{ ...}
|
||
\NormalTok{]}
|
||
|
||
\NormalTok{...}
|
||
|
||
\NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{}
|
||
\StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10}
|
||
\NormalTok{\}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
A ce stade, en nous rendant sur l'URL
|
||
\texttt{http://localhost:8000/api/v1}, nous obtiendrons ceci:
|
||
|
||
\includegraphics{images/rest/api-first-example.png}
|
||
|
||
\hypertarget{_moduxe8les_et_relations}{%
|
||
\section{Modèles et relations}\label{_moduxe8les_et_relations}}
|
||
|
||
Plus haut, nous avons utilisé une relation de type
|
||
\texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour
|
||
autoriser des relations entre vos instances à partir de l'API, mais il
|
||
faut reconnaître que cela reste assez limité. Pour palier à ceci, il
|
||
existe {[}plusieurs manières de représenter ces
|
||
relations{]}(\url{https://www.django-rest-framework.org/api-guide/relations/}):
|
||
soit \textbf{via} un hyperlien, comme ci-dessus, soit en utilisant les
|
||
clés primaires, soit en utilisant l'URL canonique permettant d'accéder à
|
||
la ressource. La solution la plus complète consiste à intégrer la
|
||
relation directement au niveau des données sérialisées, ce qui nous
|
||
permet de passer de ceci (au niveau des contrats):
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\FunctionTok{\{}
|
||
\DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,}
|
||
\DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
|
||
\DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
|
||
\DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[}
|
||
\FunctionTok{\{}
|
||
\DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,}
|
||
\DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,}
|
||
\DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[}
|
||
\StringTok{"http://localhost:8000/api/v1/contracts/1/"}\OtherTok{,}
|
||
\StringTok{"http://localhost:8000/api/v1/contracts/2/"}
|
||
\OtherTok{]}
|
||
\FunctionTok{\}}
|
||
\OtherTok{]}
|
||
\FunctionTok{\}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
à ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\FunctionTok{\{}
|
||
\DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,}
|
||
\DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
|
||
\DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
|
||
\DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[}
|
||
\FunctionTok{\{}
|
||
\DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,}
|
||
\DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,}
|
||
\DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[}
|
||
\FunctionTok{\{}
|
||
\DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2019{-}01{-}01"}\FunctionTok{,}
|
||
\DataTypeTok{"date\_end"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,}
|
||
\DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"}
|
||
\FunctionTok{\}}\OtherTok{,}
|
||
\FunctionTok{\{}
|
||
\DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2009{-}01{-}01"}\FunctionTok{,}
|
||
\DataTypeTok{"date\_end"}\FunctionTok{:} \StringTok{"2021{-}01{-}01"}\FunctionTok{,}
|
||
\DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"}
|
||
\FunctionTok{\}}
|
||
\OtherTok{]}
|
||
\FunctionTok{\}}
|
||
\OtherTok{]}
|
||
\FunctionTok{\}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
La modification se limite à \textbf{surcharger} la propriété, pour
|
||
indiquer qu'elle consiste en une instance d'un des sérialiseurs
|
||
existants. Nous passons ainsi de ceci
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):}
|
||
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ People}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
à ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):}
|
||
\NormalTok{ contract\_set }\OperatorTok{=}\NormalTok{ ContractSerializer(many}\OperatorTok{=}\VariableTok{True}\NormalTok{, read\_only}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
|
||
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ People}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Nous ne faisons donc bien que redéfinir la propriété
|
||
\texttt{contract\_set} et indiquons qu'il s'agit à présent d'une
|
||
instance de \texttt{ContractSerializer}, et qu'il est possible d'en
|
||
avoir plusieurs. C'est tout.
|
||
|
||
\hypertarget{_filtres_et_recherches}{%
|
||
\section{Filtres et recherches}\label{_filtres_et_recherches}}
|
||
|
||
A ce stade, nous pouvons juste récupérer des informations présentes dans
|
||
notre base de données, mais à part les parcourir, il est difficile d'en
|
||
faire quelque chose.
|
||
|
||
Il est possible de jouer avec les URLs en définissant une nouvelle route
|
||
ou avec les paramètres de l'URL, ce qui demanderait alors de programmer
|
||
chaque cas possible - sans que le consommateur ne puisse les déduire
|
||
lui-même. Une solution élégante consiste à autoriser le consommateur à
|
||
filtrer les données, directement au niveau de l'API. Ceci peut être
|
||
fait. Il existe deux manières de restreindre l'ensemble des résultats
|
||
retournés:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Soit au travers d'une recherche, qui permet d'effectuer une recherche
|
||
textuelle, globale et par ensemble à un ensemble de champs,
|
||
\item
|
||
Soit au travers d'un filtre, ce qui permet de spécifier une valeur
|
||
précise à rechercher.
|
||
\end{enumerate}
|
||
|
||
Dans notre exemple, la première possibilité sera utile pour rechercher
|
||
une personne répondant à un ensemble de critères. Typiquement,
|
||
\texttt{/api/v1/people/?search=raymond\ bond} ne nous donnera aucun
|
||
résultat, alors que \texttt{/api/v1/people/?search=james\ bond} nous
|
||
donnera le célèbre agent secret (qui a bien entendu un contrat chez
|
||
nous\ldots\hspace{0pt}).
|
||
|
||
Le second cas permettra par contre de préciser que nous souhaitons
|
||
disposer de toutes les personnes dont le contrat est ultérieur à une
|
||
date particulière.
|
||
|
||
Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de
|
||
proposer immédiatement les champs, et donc d'informer le consommateur
|
||
des possibilités:
|
||
|
||
\includegraphics{images/rest/drf-filters-and-searches.png}
|
||
|
||
\hypertarget{_recherches}{%
|
||
\subsection{Recherches}\label{_recherches}}
|
||
|
||
La fonction de recherche est déjà implémentée au niveau de
|
||
Django-Rest-Framework, et aucune dépendance supplémentaire n'est
|
||
nécessaire. Au niveau du \texttt{viewset}, il suffit d'ajouter deux
|
||
informations:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{...}
|
||
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ filters, viewsets}
|
||
\NormalTok{...}
|
||
|
||
\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):}
|
||
\NormalTok{ ...}
|
||
\NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [filters.SearchFilter]}
|
||
\NormalTok{ search\_fields }\OperatorTok{=}\NormalTok{ [}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{]}
|
||
\NormalTok{ ...}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_filtres}{%
|
||
\subsection{Filtres}\label{_filtres}}
|
||
|
||
Nous commençons par installer {[}le paquet
|
||
\texttt{django-filter}{]}(\url{https://www.django-rest-framework.org/api-guide/filtering/\#djangofilterbackend})
|
||
et nous l'ajoutons parmi les applications installées:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{λ }\ExtensionTok{pip}\NormalTok{ install django{-}filter}
|
||
\ExtensionTok{Collecting}\NormalTok{ django{-}filter}
|
||
\ExtensionTok{Downloading}\NormalTok{ django\_filter{-}2.4.0{-}py3{-}none{-}any.whl (73 kB)}
|
||
\KeywordTok{|}\NormalTok{████████████████████████████████}\KeywordTok{|} \ExtensionTok{73}\NormalTok{ kB 2.6 MB/s}
|
||
\ExtensionTok{Requirement}\NormalTok{ already satisfied: Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from django{-}filter) }\KeywordTok{(}\ExtensionTok{3.1.7}\KeywordTok{)}
|
||
\ExtensionTok{Requirement}\NormalTok{ already satisfied: asgiref}\OperatorTok{\textless{}}\NormalTok{4,}\OperatorTok{\textgreater{}}\NormalTok{=3.2.10 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{3.3.1}\KeywordTok{)}
|
||
\ExtensionTok{Requirement}\NormalTok{ already satisfied: sqlparse}\OperatorTok{\textgreater{}}\NormalTok{=0.2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{0.4.1}\KeywordTok{)}
|
||
\ExtensionTok{Requirement}\NormalTok{ already satisfied: pytz in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{2021.1}\KeywordTok{)}
|
||
\ExtensionTok{Installing}\NormalTok{ collected packages: django{-}filter}
|
||
\ExtensionTok{Successfully}\NormalTok{ installed django{-}filter{-}2.4.0}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Une fois l'installée réalisée, il reste deux choses à faire:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Ajouter \texttt{django\_filters} parmi les applications installées:
|
||
\item
|
||
Configurer la clé \texttt{DEFAULT\_FILTER\_BACKENDS} à la valeur
|
||
\texttt{{[}\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}{]}}.
|
||
\end{enumerate}
|
||
|
||
Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier
|
||
\texttt{settings.py} de la manière suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{}
|
||
\StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}DEFAULT\_FILTER\_BACKENDS\textquotesingle{}}\NormalTok{: [}\StringTok{\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}}\NormalTok{]}
|
||
\NormalTok{\}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Au niveau du viewset, il convient d'ajouter ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{...}
|
||
\ImportTok{from}\NormalTok{ django\_filters.rest\_framework }\ImportTok{import}\NormalTok{ DjangoFilterBackend}
|
||
\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets}
|
||
\NormalTok{...}
|
||
|
||
\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):}
|
||
\NormalTok{ ...}
|
||
\NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [DjangoFilterBackend]}
|
||
\NormalTok{ filterset\_fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}last\_name\textquotesingle{}}\NormalTok{,)}
|
||
\NormalTok{ ...}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
A ce stade, nous avons deux problèmes:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Le champ que nous avons défini au niveau de la propriété
|
||
\texttt{filterset\_fields} exige une correspondance exacte. Ainsi,
|
||
\texttt{/api/v1/people/?last\_name=Bon} ne retourne rien, alors que
|
||
\texttt{/api/v1/people/?last\_name=Bond} nous donnera notre agent
|
||
secret préféré.
|
||
\item
|
||
Il n'est pas possible d'aller appliquer un critère de sélection sur la
|
||
propriété d'une relation. Notre exemple proposant rechercher
|
||
uniquement les relations dans le futur (ou dans le passé) tombe à
|
||
l'eau.
|
||
\end{enumerate}
|
||
|
||
Pour ces deux points, nous allons définir un nouveau filtre, en
|
||
surchargeant une nouvelle classe dont la classe mère serait de type
|
||
\texttt{django\_filters.FilterSet}.
|
||
|
||
TO BE CONTINUED.
|
||
|
||
A noter qu'il existe un paquet
|
||
{[}Django-Rest-Framework-filters{]}(\url{https://github.com/philipn/django-rest-framework-filters}),
|
||
mais il est déprécié depuis Django 3.0, puisqu'il se base sur
|
||
\texttt{django.utils.six} qui n'existe à présent plus. Il faut donc le
|
||
faire à la main (ou patcher le paquet\ldots\hspace{0pt}).
|
||
|
||
\hypertarget{_urls_et_espaces_de_noms}{%
|
||
\section{URLs et espaces de noms}\label{_urls_et_espaces_de_noms}}
|
||
|
||
La gestion des URLs permet \textbf{grosso modo} d'assigner une adresse
|
||
paramétrée ou non à une fonction Python. La manière simple consiste à
|
||
modifier le fichier \texttt{gwift/settings.py} pour y ajouter nos
|
||
correspondances. Par défaut, le fichier ressemble à ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# gwift/urls.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url}
|
||
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
|
||
|
||
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),}
|
||
\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
La variable \texttt{urlpatterns} associe un ensemble d'adresses à des
|
||
fonctions. Dans le fichier \textbf{nu}, seul le \textbf{pattern}
|
||
\texttt{admin} est défini, et inclut toutes les adresses qui sont
|
||
définies dans le fichier \texttt{admin.site.urls}.
|
||
|
||
Django fonctionne avec des \textbf{expressions rationnelles} simplifiées
|
||
(des \textbf{expressions régulières} ou \textbf{regex}) pour trouver une
|
||
correspondance entre une URL et la fonction qui recevra la requête et
|
||
retournera une réponse. Nous utilisons l'expression \texttt{\^{}\$} pour
|
||
déterminer la racine de notre application, mais nous pourrions appliquer
|
||
d'autres regroupements (\texttt{/home},
|
||
\texttt{users/\textless{}profile\_id\textgreater{}},
|
||
\texttt{articles/\textless{}year\textgreater{}/\textless{}month\textgreater{}/\textless{}day\textgreater{}},
|
||
\ldots\hspace{0pt}). Chaque \textbf{variable} déclarée dans l'expression
|
||
régulière sera apparenté à un paramètre dans la fonction correspondante.
|
||
Ainsi,
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# admin.site.urls.py}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Pour reprendre l'exemple où on en était resté:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# gwift/urls.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url}
|
||
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
|
||
|
||
\ImportTok{from}\NormalTok{ wish }\ImportTok{import}\NormalTok{ views }\ImportTok{as}\NormalTok{ wish\_views}
|
||
|
||
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),}
|
||
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, wish\_views.wishlists, name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Dans la mesure du possible, essayez toujours de \textbf{nommer} chaque
|
||
expression. Cela permettra notamment de les retrouver au travers de la
|
||
fonction \texttt{reverse}, mais permettra également de simplifier vos
|
||
templates.
|
||
|
||
A présent, on doit tester que l'URL racine de notre application mène
|
||
bien vers la fonction \texttt{wish\_views.wishlists}.
|
||
|
||
Sauf que les pages \texttt{about} et \texttt{help} existent également.
|
||
Pour implémenter ce type de précédence, il faudrait implémenter les URLs
|
||
de la manière suivante:
|
||
|
||
\begin{verbatim}
|
||
| about
|
||
| help
|
||
| <user>
|
||
\end{verbatim}
|
||
|
||
Mais cela signifie aussi que les utilisateurs \texttt{about} et
|
||
\texttt{help} (s'ils existent\ldots\hspace{0pt}) ne pourront jamais
|
||
accéder à leur profil. Une dernière solution serait de maintenir une
|
||
liste d'authorité des noms d'utilisateur qu'il n'est pas possible
|
||
d'utiliser.
|
||
|
||
D'où l'importance de bien définir la séquence de déinition de ces
|
||
routes, ainsi que des espaces de noms.
|
||
|
||
Note sur les namespaces.
|
||
|
||
De là, découle une autre bonne pratique: l'utilisation de
|
||
\emph{breadcrumbs}
|
||
(\url{https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template})
|
||
ou de guidelines de navigation.
|
||
|
||
\hypertarget{_reverse}{%
|
||
\subsection{Reverse}\label{_reverse}}
|
||
|
||
En associant un nom ou un libellé à chaque URL, il est possible de
|
||
récupérer sa \textbf{traduction}. Cela implique par contre de ne plus
|
||
toucher à ce libellé par la suite\ldots\hspace{0pt}
|
||
|
||
Dans le fichier \texttt{urls.py}, on associe le libellé
|
||
\texttt{wishlists} à l'URL \texttt{r\textquotesingle{}\^{}\$}
|
||
(c'est-à-dire la racine du site):
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ wish.views }\ImportTok{import}\NormalTok{ WishListList}
|
||
|
||
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),}
|
||
\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, WishListList.as\_view(), name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
De cette manière, dans nos templates, on peut à présent construire un
|
||
lien vers la racine avec le tags suivant:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{\textless{}a}\OtherTok{ href=}\StringTok{"\{\% url \textquotesingle{}wishlists\textquotesingle{} \%\}"}\KeywordTok{\textgreater{}}\NormalTok{\{\{ yearvar \}\} Archive}\KeywordTok{\textless{}/a\textgreater{}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
De la même manière, on peut également récupérer l'URL de destination
|
||
pour n'importe quel libellé, de la manière suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse\_lazy}
|
||
|
||
\NormalTok{wishlists\_url }\OperatorTok{=}\NormalTok{ reverse\_lazy(}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_i18n_l10n}{%
|
||
\section{i18n / l10n}\label{_i18n_l10n}}
|
||
|
||
La localisation (\emph{l10n}) et l'internationalization (\emph{i18n})
|
||
sont deux concepts proches, mais différents:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Internationalisation: \emph{Preparing the software for localization.
|
||
Usually done by developers.}
|
||
\item
|
||
Localisation: \emph{Writing the translations and local formats.
|
||
Usually done by translators.}
|
||
\end{itemize}
|
||
|
||
L'internationalisation est donc le processus permettant à une
|
||
application d'accepter une forme de localisation. La seconde ne va donc
|
||
pas sans la première, tandis que la première ne fait qu'autoriser la
|
||
seconde.
|
||
|
||
\hypertarget{_arborescences}{%
|
||
\subsection{Arborescences}\label{_arborescences}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# \textless{}app\textgreater{}/management/commands/rebuild.py}
|
||
|
||
\CommentTok{"""This command manages Closure Tables implementation}
|
||
|
||
\CommentTok{It adds new levels and cleans links between entities.}
|
||
\CommentTok{This way, it\textquotesingle{}s relatively easy to fetch an entire tree with just one tiny request.}
|
||
|
||
\CommentTok{"""}
|
||
|
||
\ImportTok{from}\NormalTok{ django.core.management.base }\ImportTok{import}\NormalTok{ BaseCommand}
|
||
|
||
\ImportTok{from}\NormalTok{ rps.structure.models }\ImportTok{import}\NormalTok{ Entity, EntityTreePath}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Command(BaseCommand):}
|
||
\KeywordTok{def}\NormalTok{ handle(}\VariableTok{self}\NormalTok{, }\OperatorTok{*}\NormalTok{args, }\OperatorTok{**}\NormalTok{options):}
|
||
\NormalTok{ entities }\OperatorTok{=}\NormalTok{ Entity.objects.}\BuiltInTok{all}\NormalTok{()}
|
||
|
||
\ControlFlowTok{for}\NormalTok{ entity }\KeywordTok{in}\NormalTok{ entities:}
|
||
\NormalTok{ breadcrumb }\OperatorTok{=}\NormalTok{ [node }\ControlFlowTok{for}\NormalTok{ node }\KeywordTok{in}\NormalTok{ entity.breadcrumb()]}
|
||
|
||
\NormalTok{ tree }\OperatorTok{=} \BuiltInTok{set}\NormalTok{(EntityTreePath.objects.}\BuiltInTok{filter}\NormalTok{(descendant}\OperatorTok{=}\NormalTok{entity))}
|
||
|
||
\ControlFlowTok{for}\NormalTok{ idx, node }\KeywordTok{in} \BuiltInTok{enumerate}\NormalTok{(breadcrumb):}
|
||
\NormalTok{ tree\_path, \_ }\OperatorTok{=}\NormalTok{ EntityTreePath.objects.get\_or\_create(}
|
||
\NormalTok{ ancestor}\OperatorTok{=}\NormalTok{node, descendant}\OperatorTok{=}\NormalTok{entity, weight}\OperatorTok{=}\NormalTok{idx }\OperatorTok{+} \DecValTok{1}
|
||
\NormalTok{ )}
|
||
|
||
\ControlFlowTok{if}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:}
|
||
\NormalTok{ tree.remove(tree\_path)}
|
||
|
||
\ControlFlowTok{for}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:}
|
||
\NormalTok{ tree\_path.delete()}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_conclusions_3}{%
|
||
\section{Conclusions}\label{_conclusions_3}}
|
||
|
||
De part son pattern \texttt{MVT}, Django ne fait pas comme les autres
|
||
frameworks.
|
||
|
||
Pour commencer, nous allons nous concentrer sur la création d'un site ne
|
||
contenant qu'une seule application, même si en pratique le site
|
||
contiendra déjà plusieurs applications fournies pas django, comme nous
|
||
le verrons plus loin.
|
||
|
||
Don't make me think, or why I switched from JS SPAs to Ruby On Rails
|
||
\url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment}
|
||
|
||
Pour prendre un exemple concret, nous allons créer un site permettant de
|
||
gérer des listes de souhaits, que nous appellerons \texttt{gwift} (pour
|
||
\texttt{GiFTs\ and\ WIshlisTs} :)).
|
||
|
||
La première chose à faire est de définir nos besoins du point de vue de
|
||
l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur
|
||
puisse faire avec l'application.
|
||
|
||
Ensuite, nous pourrons traduire ces besoins en fonctionnalités et
|
||
finalement effectuer le développement.
|
||
|
||
\hypertarget{_gwift}{%
|
||
\section{Gwift}\label{_gwift}}
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/django-project-vs-apps-gwift.png}
|
||
\caption{Gwift}
|
||
\end{figure}
|
||
|
||
\hypertarget{_besoins_utilisateurs}{%
|
||
\section{Besoins utilisateurs}\label{_besoins_utilisateurs}}
|
||
|
||
Nous souhaitons développer un site où un utilisateur donné peut créer
|
||
une liste contenant des souhaits et où d'autres utilisateurs,
|
||
authentifiés ou non, peuvent choisir les souhaits à la réalisation
|
||
desquels ils souhaitent participer.
|
||
|
||
Il sera nécessaire de s'authentifier pour :
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Créer une liste associée à l'utilisateur en cours
|
||
\item
|
||
Ajouter un nouvel élément à une liste
|
||
\end{itemize}
|
||
|
||
Il ne sera pas nécessaire de s'authentifier pour :
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Faire une promesse d'offre pour un élément appartenant à une liste,
|
||
associée à un utilisateur.
|
||
\end{itemize}
|
||
|
||
L'utilisateur ayant créé une liste pourra envoyer un email directement
|
||
depuis le site aux personnes avec qui il souhaite partager sa liste, cet
|
||
email contenant un lien permettant d'accéder à cette liste.
|
||
|
||
A chaque souhait, on pourrait de manière facultative ajouter un prix.
|
||
Dans ce cas, le souhait pourrait aussi être subdivisé en plusieurs
|
||
parties, de manière à ce que plusieurs personnes puissent participer à
|
||
sa réalisation.
|
||
|
||
Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à
|
||
dupliquer le souhait en question.
|
||
|
||
\hypertarget{_besoins_fonctionnels}{%
|
||
\section{Besoins fonctionnels}\label{_besoins_fonctionnels}}
|
||
|
||
\hypertarget{_gestion_des_utilisateurs}{%
|
||
\subsection{Gestion des utilisateurs}\label{_gestion_des_utilisateurs}}
|
||
|
||
Pour gérer les utilisateurs, nous allons faire en sorte de surcharger ce
|
||
que Django propose: par défaut, on a une la possibilité de gérer des
|
||
utilisateurs (identifiés par une adresse email, un nom, un prénom,
|
||
\ldots\hspace{0pt}) mais sans plus.
|
||
|
||
Ce qu'on peut souhaiter, c'est que l'utilisateur puisse s'authentifier
|
||
grâce à une plateforme connue (Facebook, Twitter, Google, etc.), et
|
||
qu'il puisse un minimum gérer son profil.
|
||
|
||
\hypertarget{_gestion_des_listes}{%
|
||
\subsection{Gestion des listes}\label{_gestion_des_listes}}
|
||
|
||
\hypertarget{_moduxe8lisation}{%
|
||
\subsubsection{Modèlisation}\label{_moduxe8lisation}}
|
||
|
||
Les données suivantes doivent être associées à une liste:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
un identifiant
|
||
\item
|
||
un identifiant externe (un GUID, par exemple)
|
||
\item
|
||
un nom
|
||
\item
|
||
une description
|
||
\item
|
||
le propriétaire, associé à l'utilisateur qui l'aura créée
|
||
\item
|
||
une date de création
|
||
\item
|
||
une date de modification
|
||
\end{itemize}
|
||
|
||
\hypertarget{_fonctionnalituxe9s}{%
|
||
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s}}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et
|
||
supprimer une liste dont il est le propriétaire
|
||
\item
|
||
Un utilisateur doit pouvoir associer ou retirer des souhaits à une
|
||
liste dont il est le propriétaire
|
||
\item
|
||
Il faut pouvoir accéder à une liste, avec un utilisateur authentifier
|
||
ou non, \textbf{via} son identifiant externe
|
||
\item
|
||
Il faut pouvoir envoyer un email avec le lien vers la liste, contenant
|
||
son identifiant externe
|
||
\item
|
||
L'utilisateur doit pouvoir voir toutes les listes qui lui
|
||
appartiennent
|
||
\end{itemize}
|
||
|
||
\hypertarget{_gestion_des_souhaits}{%
|
||
\subsection{Gestion des souhaits}\label{_gestion_des_souhaits}}
|
||
|
||
\hypertarget{_moduxe9lisation_2}{%
|
||
\subsubsection{Modélisation}\label{_moduxe9lisation_2}}
|
||
|
||
Les données suivantes peuvent être associées à un souhait:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
un identifiant
|
||
\item
|
||
identifiant de la liste
|
||
\item
|
||
un nom
|
||
\item
|
||
une description
|
||
\item
|
||
le propriétaire
|
||
\item
|
||
une date de création
|
||
\item
|
||
une date de modification
|
||
\item
|
||
une image, afin de représenter l'objet ou l'idée
|
||
\item
|
||
un nombre (1 par défaut)
|
||
\item
|
||
un prix facultatif
|
||
\item
|
||
un nombre de part, facultatif également, si un prix est fourni.
|
||
\end{itemize}
|
||
|
||
\hypertarget{_fonctionnalituxe9s_2}{%
|
||
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_2}}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et
|
||
supprimer un souhait dont il est le propriétaire.
|
||
\item
|
||
On ne peut créer un souhait sans liste associée
|
||
\item
|
||
Il faut pouvoir fractionner un souhait uniquement si un prix est
|
||
donné.
|
||
\item
|
||
Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié
|
||
ou non.
|
||
\item
|
||
Il faut pouvoir réaliser un souhait ou une partie seulement, avec un
|
||
utilisateur authentifié ou non.
|
||
\item
|
||
Un souhait en cours de réalisation et composé de différentes parts ne
|
||
peut plus être modifié.
|
||
\item
|
||
Un souhait en cours de réalisation ou réalisé ne peut plus être
|
||
supprimé.
|
||
\item
|
||
On peut modifier le nombre de fois qu'un souhait doit être réalisé
|
||
dans la limite des réalisations déjà effectuées.
|
||
\end{itemize}
|
||
|
||
\hypertarget{_gestion_des_ruxe9alisations_de_souhaits}{%
|
||
\subsection{Gestion des réalisations de
|
||
souhaits}\label{_gestion_des_ruxe9alisations_de_souhaits}}
|
||
|
||
\hypertarget{_moduxe9lisation_3}{%
|
||
\subsubsection{Modélisation}\label{_moduxe9lisation_3}}
|
||
|
||
Les données suivantes peuvent être associées à une réalisation de
|
||
souhait:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
identifiant du souhait
|
||
\item
|
||
identifiant de l'utilisateur si connu
|
||
\item
|
||
identifiant de la personne si utilisateur non connu
|
||
\item
|
||
un commentaire
|
||
\item
|
||
une date de réalisation
|
||
\end{itemize}
|
||
|
||
\hypertarget{_fonctionnalituxe9s_3}{%
|
||
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_3}}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
L'utilisateur doit pouvoir voir si un souhait est réalisé, en partie
|
||
ou non. Il doit également avoir un pourcentage de complétion sur la
|
||
possibilité de réalisation de son souhait, entre 0\% et 100\%.
|
||
\item
|
||
L'utilisateur doit pouvoir voir la ou les personnes ayant réalisé un
|
||
souhait.
|
||
\item
|
||
Il y a autant de réalisation que de parts de souhait réalisées ou de
|
||
nombre de fois que le souhait est réalisé.
|
||
\end{itemize}
|
||
|
||
\hypertarget{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}{%
|
||
\subsection{Gestion des personnes réalisants les souhaits et qui ne sont
|
||
pas
|
||
connues}\label{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}}
|
||
|
||
\hypertarget{_moduxe9lisation_4}{%
|
||
\subsubsection{Modélisation}\label{_moduxe9lisation_4}}
|
||
|
||
Les données suivantes peuvent être associées à une personne réalisant un
|
||
souhait:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
un identifiant
|
||
\item
|
||
un nom
|
||
\item
|
||
une adresse email facultative
|
||
\end{itemize}
|
||
|
||
\hypertarget{_fonctionnalituxe9s_4}{%
|
||
\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_4}}
|
||
|
||
Modélisation
|
||
|
||
L'ORM de Django permet de travailler uniquement avec une définition de
|
||
classes, et de faire en sorte que le lien avec la base de données soit
|
||
géré uniquement de manière indirecte, par Django lui-même. On peut
|
||
schématiser ce comportement par une classe = une table.
|
||
|
||
Comme on l'a vu dans la description des fonctionnalités, on va
|
||
\textbf{grosso modo} avoir besoin des éléments suivants:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Des listes de souhaits
|
||
\item
|
||
Des éléments qui composent ces listes
|
||
\item
|
||
Des parts pouvant composer chacun de ces éléments
|
||
\item
|
||
Des utilisateurs pour gérer tout ceci.
|
||
\end{itemize}
|
||
|
||
Nous proposons dans un premier temps d'éluder la gestion des
|
||
utilisateurs, et de simplement se concentrer sur les fonctionnalités
|
||
principales. Cela nous donne ceci:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\alph{enumi}.}
|
||
\item
|
||
code-block:: python
|
||
|
||
\begin{verbatim}
|
||
# wish/models.py
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
from django.db import models
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
class Wishlist(models.Model):
|
||
pass
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
class Item(models.Model):
|
||
pass
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
class Part(models.Model):
|
||
pass
|
||
\end{verbatim}
|
||
\end{enumerate}
|
||
|
||
Les classes sont créées, mais vides. Entrons dans les détails.
|
||
|
||
Listes de souhaits
|
||
|
||
Comme déjà décrit précédemment, les listes de souhaits peuvent
|
||
s'apparenter simplement à un objet ayant un nom et une description. Pour
|
||
rappel, voici ce qui avait été défini dans les spécifications:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
un identifiant
|
||
\item
|
||
un identifiant externe
|
||
\item
|
||
un nom
|
||
\item
|
||
une description
|
||
\item
|
||
une date de création
|
||
\item
|
||
une date de modification
|
||
\end{itemize}
|
||
|
||
Notre classe \texttt{Wishlist} peut être définie de la manière suivante:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\alph{enumi}.}
|
||
\item
|
||
code-block:: python
|
||
|
||
\begin{verbatim}
|
||
# wish/models.py
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
class Wishlist(models.Model):
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
name = models.CharField(max_length=255)
|
||
description = models.TextField()
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
external_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
|
||
\end{verbatim}
|
||
\end{enumerate}
|
||
|
||
Que peut-on constater?
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Que s'il n'est pas spécifié, un identifiant \texttt{id} sera
|
||
automatiquement généré et accessible dans le modèle. Si vous souhaitez
|
||
malgré tout spécifier que ce soit un champ en particulier qui devienne
|
||
la clé primaire, il suffit de l'indiquer grâce à l'attribut
|
||
\texttt{primary\_key=True}.
|
||
\item
|
||
Que chaque type de champs (\texttt{DateTimeField}, \texttt{CharField},
|
||
\texttt{UUIDField}, etc.) a ses propres paramètres d'initialisation.
|
||
Il est intéressant de les apprendre ou de se référer à la
|
||
documentation en cas de doute.
|
||
\end{itemize}
|
||
|
||
Au niveau de notre modélisation:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
La propriété \texttt{created\_at} est gérée automatiquement par Django
|
||
grâce à l'attribut \texttt{auto\_now\_add}: de cette manière, lors
|
||
d'un \textbf{ajout}, une valeur par défaut ("\textbf{maintenant}")
|
||
sera attribuée à cette propriété.
|
||
\item
|
||
La propriété \texttt{updated\_at} est également gérée automatique,
|
||
cette fois grâce à l'attribut \texttt{auto\_now} initialisé à
|
||
\texttt{True}: lors d'une \textbf{mise à jour}, la propriété se verra
|
||
automatiquement assigner la valeur du moment présent. Cela ne permet
|
||
évidemment pas de gérer un historique complet et ne nous dira pas
|
||
\textbf{quels champs} ont été modifiés, mais cela nous conviendra dans
|
||
un premier temps.
|
||
\item
|
||
La propriété \texttt{external\_id} est de type \texttt{UUIDField}.
|
||
Lorsqu'une nouvelle instance sera instanciée, cette propriété prendra
|
||
la valeur générée par la fonction \texttt{uuid.uuid4()}. \textbf{A
|
||
priori}, chacun des types de champs possède une propriété
|
||
\texttt{default}, qui permet d'initialiser une valeur sur une nouvelle
|
||
instance.
|
||
\end{itemize}
|
||
|
||
Souhaits
|
||
|
||
Nos souhaits ont besoin des propriétés suivantes:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
un identifiant
|
||
\item
|
||
l'identifiant de la liste auquel le souhait est lié
|
||
\item
|
||
un nom
|
||
\item
|
||
une description
|
||
\item
|
||
le propriétaire
|
||
\item
|
||
une date de création
|
||
\item
|
||
une date de modification
|
||
\item
|
||
une image permettant de le représenter.
|
||
\item
|
||
un nombre (1 par défaut)
|
||
\item
|
||
un prix facultatif
|
||
\item
|
||
un nombre de part facultatif, si un prix est fourni.
|
||
\end{itemize}
|
||
|
||
Après implémentation, cela ressemble à ceci:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\alph{enumi}.}
|
||
\item
|
||
code-block:: python
|
||
|
||
\begin{verbatim}
|
||
# wish/models.py
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
class Wish(models.Model):
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
wishlist = models.ForeignKey(Wishlist)
|
||
name = models.CharField(max_length=255)
|
||
description = models.TextField()
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
picture = models.ImageField()
|
||
numbers_available = models.IntegerField(default=1)
|
||
number_of_parts = models.IntegerField(null=True)
|
||
estimated_price = models.DecimalField(max_digits=19, decimal_places=2,
|
||
null=True)
|
||
\end{verbatim}
|
||
\end{enumerate}
|
||
|
||
A nouveau, que peut-on constater ?
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Les clés étrangères sont gérées directement dans la déclaration du
|
||
modèle. Un champ de type `ForeignKey
|
||
\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/models/fields/\#django.db.models.ForeignKey\%3E\%60_}
|
||
permet de déclarer une relation 1-N entre deux classes. Dans la même
|
||
veine, une relation 1-1 sera représentée par un champ de type
|
||
`OneToOneField
|
||
\textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/\%3E\%60}\emph{,
|
||
alors qu'une relation N-N utilisera un `ManyToManyField
|
||
\textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/\%3E\%60}}.
|
||
\item
|
||
L'attribut \texttt{default} permet de spécifier une valeur initiale,
|
||
utilisée lors de la construction de l'instance. Cet attribut peut
|
||
également être une fonction.
|
||
\item
|
||
Pour rendre un champ optionnel, il suffit de lui ajouter l'attribut
|
||
\texttt{null=True}.
|
||
\item
|
||
Comme cité ci-dessus, chaque champ possède des attributs spécifiques.
|
||
Le champ \texttt{DecimalField} possède par exemple les attributs
|
||
\texttt{max\_digits} et \texttt{decimal\_places}, qui nous permettra
|
||
de représenter une valeur comprise entre 0 et plus d'un milliard (avec
|
||
deux chiffres décimaux).
|
||
\item
|
||
L'ajout d'un champ de type \texttt{ImageField} nécessite
|
||
l'installation de \texttt{pillow} pour la gestion des images. Nous
|
||
l'ajoutons donc à nos pré-requis, dans le fichier
|
||
\texttt{requirements/base.txt}.
|
||
\end{itemize}
|
||
|
||
Parts
|
||
|
||
Les parts ont besoins des propriétés suivantes:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
un identifiant
|
||
\item
|
||
identifiant du souhait
|
||
\item
|
||
identifiant de l'utilisateur si connu
|
||
\item
|
||
identifiant de la personne si utilisateur non connu
|
||
\item
|
||
un commentaire
|
||
\item
|
||
une date de réalisation
|
||
\end{itemize}
|
||
|
||
Elles constituent la dernière étape de notre modélisation et représente
|
||
la réalisation d'un souhait. Il y aura autant de part d'un souhait que
|
||
le nombre de souhait à réaliser fois le nombre de part.
|
||
|
||
Elles permettent à un utilisateur de participer au souhait émis par un
|
||
autre utilisateur. Pour les modéliser, une part est liée d'un côté à un
|
||
souhait, et d'autre part à un utilisateur. Cela nous donne ceci:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\alph{enumi}.}
|
||
\item
|
||
code-block:: python
|
||
|
||
\begin{verbatim}
|
||
from django.contrib.auth.models import User
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
class WishPart(models.Model):
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
wish = models.ForeignKey(Wish)
|
||
user = models.ForeignKey(User, null=True)
|
||
unknown_user = models.ForeignKey(UnknownUser, null=True)
|
||
comment = models.TextField(null=True, blank=True)
|
||
done_at = models.DateTimeField(auto_now_add=True)
|
||
\end{verbatim}
|
||
\end{enumerate}
|
||
|
||
La classe \texttt{User} référencée au début du snippet correspond à
|
||
l'utilisateur qui sera connecté. Ceci est géré par Django. Lorsqu'une
|
||
requête est effectuée et est transmise au serveur, cette information
|
||
sera disponible grâce à l'objet \texttt{request.user}, transmis à chaque
|
||
fonction ou \textbf{Class-based-view}. C'est un des avantages d'un
|
||
framework tout intégré: il vient \textbf{batteries-included} et beaucoup
|
||
de détails ne doivent pas être pris en compte. Pour le moment, nous nous
|
||
limiterons à ceci. Par la suite, nous verrons comment améliorer la
|
||
gestion des profils utilisateurs, comment y ajouter des informations et
|
||
comment gérer les cas particuliers.
|
||
|
||
La classe \texttt{UnknownUser} permet de représenter un utilisateur non
|
||
enregistré sur le site et est définie au point suivant.
|
||
|
||
Utilisateurs inconnus
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\alph{enumi}.}
|
||
\item
|
||
todo:: je supprimerais pour que tous les utilisateurs soient gérés au
|
||
même endroit.
|
||
\end{enumerate}
|
||
|
||
Pour chaque réalisation d'un souhait par quelqu'un, il est nécessaire de
|
||
sauver les données suivantes, même si l'utilisateur n'est pas enregistré
|
||
sur le site:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
un identifiant
|
||
\item
|
||
un nom
|
||
\item
|
||
une adresse email. Cette adresse email sera unique dans notre base de
|
||
données, pour ne pas créer une nouvelle occurence si un même
|
||
utilisateur participe à la réalisation de plusieurs souhaits.
|
||
\end{itemize}
|
||
|
||
Ceci nous donne après implémentation:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\alph{enumi}.}
|
||
\item
|
||
code-block:: python
|
||
|
||
\begin{verbatim}
|
||
class UnkownUser(models.Model):
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
name = models.CharField(max_length=255)
|
||
email = models.CharField(email = models.CharField(max_length=255, unique=True)
|
||
\end{verbatim}
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_tests_unitaires_2}{%
|
||
\section{Tests unitaires}\label{_tests_unitaires_2}}
|
||
|
||
\hypertarget{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}{%
|
||
\subsection{Pourquoi s'ennuyer à écrire des
|
||
tests?}\label{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}}
|
||
|
||
Traduit grossièrement depuis un article sur `https://medium.com
|
||
\textless{}\url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_}:
|
||
|
||
\begin{verbatim}
|
||
Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:
|
||
\end{verbatim}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Aide au design: écrire des tests avant d'écrire le code vous donnera
|
||
une meilleure perspective sur le design à appliquer aux API.
|
||
\item
|
||
Documentation (pour les développeurs): chaque description d'un test
|
||
\item
|
||
Tester votre compréhension en tant que développeur:
|
||
\item
|
||
Assurance qualité: des tests, 5.
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_why_bother_with_test_discipline}{%
|
||
\subsection{Why Bother with Test
|
||
Discipline?}\label{_why_bother_with_test_discipline}}
|
||
|
||
Your tests are your first and best line of defense against software
|
||
defects. Your tests are more important than linting \& static analysis
|
||
(which can only find a subclass of errors, not problems with your actual
|
||
program logic). Tests are as important as the implementation itself (all
|
||
that matters is that the code meets the requirement --- how it's
|
||
implemented doesn't matter at all unless it's implemented poorly).
|
||
|
||
Unit tests combine many features that make them your secret weapon to
|
||
application success:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Design aid: Writing tests first gives you a clearer perspective on the
|
||
ideal API design.
|
||
\item
|
||
Feature documentation (for developers): Test descriptions enshrine in
|
||
code every implemented feature requirement.
|
||
\item
|
||
Test your developer understanding: Does the developer understand the
|
||
problem enough to articulate in code all critical component
|
||
requirements?
|
||
\item
|
||
Quality Assurance: Manual QA is error prone. In my experience, it's
|
||
impossible for a developer to remember all features that need testing
|
||
after making a change to refactor, add new features, or remove
|
||
features.
|
||
\item
|
||
Continuous Delivery Aid: Automated QA affords the opportunity to
|
||
automatically prevent broken builds from being deployed to production.
|
||
\end{enumerate}
|
||
|
||
Unit tests don't need to be twisted or manipulated to serve all of those
|
||
broad-ranging goals. Rather, it is in the essential nature of a unit
|
||
test to satisfy all of those needs. These benefits are all side-effects
|
||
of a well-written test suite with good coverage.
|
||
|
||
\hypertarget{_what_are_you_testing}{%
|
||
\subsection{What are you testing?}\label{_what_are_you_testing}}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
What component aspect are you testing?
|
||
\item
|
||
What should the feature do? What specific behavior requirement are you
|
||
testing?
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_couverture_de_code_2}{%
|
||
\subsection{Couverture de code}\label{_couverture_de_code_2}}
|
||
|
||
On a vu au chapitre 1 qu'il était possible d'obtenir une couverture de
|
||
code, c'est-à-dire un pourcentage.
|
||
|
||
\hypertarget{_comment_tester}{%
|
||
\subsection{Comment tester ?}\label{_comment_tester}}
|
||
|
||
Il y a deux manières d'écrire les tests: soit avant, soit après
|
||
l'implémentation. Oui, idéalement, les tests doivent être écrits à
|
||
l'avance. Entre nous, on ne va pas râler si vous faites l'inverse,
|
||
l'important étant que vous le fassiez. Une bonne métrique pour vérifier
|
||
l'avancement des tests est la couverture de code.
|
||
|
||
Pour l'exemple, nous allons écrire la fonction
|
||
\texttt{percentage\_of\_completion} sur la classe \texttt{Wish}, et nous
|
||
allons spécifier les résultats attendus avant même d'implémenter son
|
||
contenu. Prenons le cas où nous écrivons la méthode avant son test:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Wish(models.Model):}
|
||
|
||
\NormalTok{ [...]}
|
||
|
||
\AttributeTok{@property}
|
||
\KeywordTok{def}\NormalTok{ percentage\_of\_completion(}\VariableTok{self}\NormalTok{):}
|
||
\CommentTok{"""}
|
||
\CommentTok{ Calcule le pourcentage de complétion pour un élément.}
|
||
\CommentTok{ """}
|
||
\NormalTok{ number\_of\_linked\_parts }\OperatorTok{=}\NormalTok{ WishPart.objects.}\BuiltInTok{filter}\NormalTok{(wish}\OperatorTok{=}\VariableTok{self}\NormalTok{).count()}
|
||
\NormalTok{ total }\OperatorTok{=} \VariableTok{self}\NormalTok{.number\_of\_parts }\OperatorTok{*} \VariableTok{self}\NormalTok{.numbers\_available}
|
||
\NormalTok{ percentage }\OperatorTok{=}\NormalTok{ (number\_of\_linked\_parts }\OperatorTok{/}\NormalTok{ total)}
|
||
\ControlFlowTok{return}\NormalTok{ percentage }\OperatorTok{*} \DecValTok{100}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
|
||
|
||
\begin{verbatim}
|
||
$ coverage run --source "." src/manage.py test wish
|
||
$ coverage report
|
||
|
||
Name Stmts Miss Branch BrPart Cover
|
||
------------------------------------------------------------------
|
||
src\gwift\__init__.py 0 0 0 0 100%
|
||
src\gwift\settings\__init__.py 4 0 0 0 100%
|
||
src\gwift\settings\base.py 14 0 0 0 100%
|
||
src\gwift\settings\dev.py 8 0 2 0 100%
|
||
src\manage.py 6 0 2 1 88%
|
||
src\wish\__init__.py 0 0 0 0 100%
|
||
src\wish\admin.py 1 0 0 0 100%
|
||
src\wish\models.py 36 5 0 0 88%
|
||
------------------------------------------------------------------
|
||
TOTAL 69 5 4 1 93%
|
||
\end{verbatim}
|
||
|
||
Si vous générez le rapport HTML avec la commande \texttt{coverage\ html}
|
||
et que vous ouvrez le fichier
|
||
\texttt{coverage\_html\_report/src\_wish\_models\_py.html}, vous verrez
|
||
que les méthodes en rouge ne sont pas testées. \textbf{A contrario}, la
|
||
couverture de code atteignait \textbf{98\%} avant l'ajout de cette
|
||
nouvelle méthode.
|
||
|
||
Pour cela, on va utiliser un fichier \texttt{tests.py} dans notre
|
||
application \texttt{wish}. \textbf{A priori}, ce fichier est créé
|
||
automatiquement lorsque vous initialisez une nouvelle application.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
|
||
|
||
\KeywordTok{class}\NormalTok{ TestWishModel(TestCase):}
|
||
\KeywordTok{def}\NormalTok{ test\_percentage\_of\_completion(}\VariableTok{self}\NormalTok{):}
|
||
\CommentTok{"""}
|
||
\CommentTok{ Vérifie que le pourcentage de complétion d\textquotesingle{}un souhait}
|
||
\CommentTok{ est correctement calculé.}
|
||
|
||
\CommentTok{ Sur base d\textquotesingle{}un souhait, on crée quatre parts et on vérifie}
|
||
\CommentTok{ que les valeurs s\textquotesingle{}étalent correctement sur 25\%, 50\%, 75\% et 100\%.}
|
||
\CommentTok{ """}
|
||
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist(name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake WishList\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wishlist\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{ wishlist.save()}
|
||
|
||
\NormalTok{ wish }\OperatorTok{=}\NormalTok{ Wish(wishlist}\OperatorTok{=}\NormalTok{wishlist,}
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake Wish\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wish\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ number\_of\_parts}\OperatorTok{=}\DecValTok{4}\NormalTok{)}
|
||
\NormalTok{ wish.save()}
|
||
|
||
\NormalTok{ part1 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part1\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{ part1.save()}
|
||
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{25}\NormalTok{, wish.percentage\_of\_completion)}
|
||
|
||
\NormalTok{ part2 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part2\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{ part2.save()}
|
||
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{50}\NormalTok{, wish.percentage\_of\_completion)}
|
||
|
||
\NormalTok{ part3 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part3\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{ part3.save()}
|
||
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{75}\NormalTok{, wish.percentage\_of\_completion)}
|
||
|
||
\NormalTok{ part4 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part4\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{ part4.save()}
|
||
\VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{100}\NormalTok{, wish.percentage\_of\_completion)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
L'attribut \texttt{@property} sur la méthode
|
||
\texttt{percentage\_of\_completion()} va nous permettre d'appeler
|
||
directement la méthode \texttt{percentage\_of\_completion()} comme s'il
|
||
s'agissait d'une propriété de la classe, au même titre que les champs
|
||
\texttt{number\_of\_parts} ou \texttt{numbers\_available}. Attention que
|
||
ce type de méthode contactera la base de données à chaque fois qu'elle
|
||
sera appelée. Il convient de ne pas surcharger ces méthodes de
|
||
connexions à la base: sur de petites applications, ce type de
|
||
comportement a très peu d'impacts, mais ce n'est plus le cas sur de
|
||
grosses applications ou sur des méthodes fréquemment appelées. Il
|
||
convient alors de passer par un mécanisme de \textbf{cache}, que nous
|
||
aborderons plus loin.
|
||
|
||
En relançant la couverture de code, on voit à présent que nous arrivons
|
||
à 99\%:
|
||
|
||
\begin{verbatim}
|
||
$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
|
||
.
|
||
----------------------------------------------------------------------
|
||
Ran 1 test in 0.006s
|
||
|
||
OK
|
||
Creating test database for alias 'default'...
|
||
Destroying test database for alias 'default'...
|
||
Name Stmts Miss Branch BrPart Cover
|
||
------------------------------------------------------------------
|
||
src\gwift\__init__.py 0 0 0 0 100%
|
||
src\gwift\settings\__init__.py 4 0 0 0 100%
|
||
src\gwift\settings\base.py 14 0 0 0 100%
|
||
src\gwift\settings\dev.py 8 0 2 0 100%
|
||
src\manage.py 6 0 2 1 88%
|
||
src\wish\__init__.py 0 0 0 0 100%
|
||
src\wish\admin.py 1 0 0 0 100%
|
||
src\wish\models.py 34 0 0 0 100%
|
||
src\wish\tests.py 20 0 0 0 100%
|
||
------------------------------------------------------------------
|
||
TOTAL 87 0 4 1 99%
|
||
\end{verbatim}
|
||
|
||
En continuant de cette manière (ie. Ecriture du code et des tests,
|
||
vérification de la couverture de code), on se fixe un objectif idéal dès
|
||
le début du projet. En prenant un développement en cours de route,
|
||
fixez-vous comme objectif de ne jamais faire baisser la couverture de
|
||
code.
|
||
|
||
\hypertarget{_quelques_liens_utiles}{%
|
||
\subsection{Quelques liens utiles}\label{_quelques_liens_utiles}}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
`Django factory boy
|
||
\textless{}\url{https://github.com/rbarrois/django-factory_boy/tree/v1.0.0\%3E\%60_}
|
||
\end{itemize}
|
||
|
||
\hypertarget{_refactoring}{%
|
||
\section{Refactoring}\label{_refactoring}}
|
||
|
||
On constate que plusieurs classes possèdent les mêmes propriétés
|
||
\texttt{created\_at} et \texttt{updated\_at}, initialisées aux mêmes
|
||
valeurs. Pour gagner en cohérence, nous allons créer une classe dans
|
||
laquelle nous définirons ces deux champs, et nous ferons en sorte que
|
||
les classes \texttt{Wishlist}, \texttt{Item} et \texttt{Part} en
|
||
héritent. Django gère trois sortes d'héritage:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
L'héritage par classe abstraite
|
||
\item
|
||
L'héritage classique
|
||
\item
|
||
L'héritage par classe proxy.
|
||
\end{itemize}
|
||
|
||
\hypertarget{_classe_abstraite}{%
|
||
\subsection{Classe abstraite}\label{_classe_abstraite}}
|
||
|
||
L'héritage par classe abstraite consiste à déterminer une classe mère
|
||
qui ne sera jamais instanciée. C'est utile pour définir des champs qui
|
||
se répèteront dans plusieurs autres classes et surtout pour respecter le
|
||
principe de DRY. Comme la classe mère ne sera jamais instanciée, ces
|
||
champs seront en fait dupliqués physiquement, et traduits en SQL, dans
|
||
chacune des classes filles.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# wish/models.py}
|
||
|
||
\KeywordTok{class}\NormalTok{ AbstractModel(models.Model):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ abstract }\OperatorTok{=} \VariableTok{True}
|
||
|
||
\NormalTok{ created\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now\_add}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
|
||
\NormalTok{ updated\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Wishlist(AbstractModel):}
|
||
\ControlFlowTok{pass}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Item(AbstractModel):}
|
||
\ControlFlowTok{pass}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Part(AbstractModel):}
|
||
\ControlFlowTok{pass}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
En traduisant ceci en SQL, on aura en fait trois tables, chacune
|
||
reprenant les champs \texttt{created\_at} et \texttt{updated\_at}, ainsi
|
||
que son propre identifiant:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{{-}{-}$ python manage.py sql wish}
|
||
\ControlFlowTok{BEGIN}\NormalTok{;}
|
||
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (}
|
||
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
|
||
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
|
||
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
|
||
\NormalTok{)}
|
||
\NormalTok{;}
|
||
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (}
|
||
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
|
||
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
|
||
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
|
||
\NormalTok{)}
|
||
\NormalTok{;}
|
||
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (}
|
||
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
|
||
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
|
||
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
|
||
\NormalTok{)}
|
||
\NormalTok{;}
|
||
|
||
\KeywordTok{COMMIT}\NormalTok{;}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_huxe9ritage_classique}{%
|
||
\subsection{Héritage classique}\label{_huxe9ritage_classique}}
|
||
|
||
L'héritage classique est généralement déconseillé, car il peut
|
||
introduire très rapidement un problème de performances: en reprenant
|
||
l'exemple introduit avec l'héritage par classe abstraite, et en omettant
|
||
l'attribut \texttt{abstract\ =\ True}, on se retrouvera en fait avec
|
||
quatre tables SQL:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Une table \texttt{AbstractModel}, qui reprend les deux champs
|
||
\texttt{created\_at} et \texttt{updated\_at}
|
||
\item
|
||
Une table \texttt{Wishlist}
|
||
\item
|
||
Une table \texttt{Item}
|
||
\item
|
||
Une table \texttt{Part}.
|
||
\end{itemize}
|
||
|
||
A nouveau, en analysant la sortie SQL de cette modélisation, on obtient
|
||
ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{{-}{-}$ python manage.py sql wish}
|
||
|
||
\ControlFlowTok{BEGIN}\NormalTok{;}
|
||
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}
|
||
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
|
||
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
|
||
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
|
||
\NormalTok{)}
|
||
\NormalTok{;}
|
||
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (}
|
||
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
|
||
\NormalTok{)}
|
||
\NormalTok{;}
|
||
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (}
|
||
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
|
||
\NormalTok{)}
|
||
\NormalTok{;}
|
||
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (}
|
||
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
|
||
\NormalTok{)}
|
||
\NormalTok{;}
|
||
|
||
\KeywordTok{COMMIT}\NormalTok{;}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Le problème est que les identifiants seront définis et incrémentés au
|
||
niveau de la table mère. Pour obtenir les informations héritées, nous
|
||
seront obligés de faire une jointure. En gros, impossible d'obtenir les
|
||
données complètes pour l'une des classes de notre travail de base sans
|
||
effectuer un \textbf{join} sur la classe mère.
|
||
|
||
Dans ce sens, cela va encore\ldots\hspace{0pt} Mais imaginez que vous
|
||
définissiez une classe \texttt{Wishlist}, de laquelle héritent les
|
||
classes \texttt{ChristmasWishlist} et \texttt{EasterWishlist}: pour
|
||
obtenir la liste complètes des listes de souhaits, il vous faudra faire
|
||
une jointure \textbf{externe} sur chacune des tables possibles, avant
|
||
même d'avoir commencé à remplir vos données. Il est parfois nécessaire
|
||
de passer par cette modélisation, mais en étant conscient des risques
|
||
inhérents.
|
||
|
||
\hypertarget{_classe_proxy}{%
|
||
\subsection{Classe proxy}\label{_classe_proxy}}
|
||
|
||
Lorsqu'on définit une classe de type \textbf{proxy}, on fait en sorte
|
||
que cette nouvelle classe ne définisse aucun nouveau champ sur la classe
|
||
mère. Cela ne change dès lors rien à la traduction du modèle de données
|
||
en SQL, puisque la classe mère sera traduite par une table, et la classe
|
||
fille ira récupérer les mêmes informations dans la même table: elle ne
|
||
fera qu'ajouter ou modifier un comportement dynamiquement, sans ajouter
|
||
d'emplacements de stockage supplémentaires.
|
||
|
||
Nous pourrions ainsi définir les classes suivantes:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# wish/models.py}
|
||
|
||
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ description }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{2000}\NormalTok{)}
|
||
\NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ models.DateField()}
|
||
|
||
\AttributeTok{@staticmethod}
|
||
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description, expiration\_date}\OperatorTok{=}\VariableTok{None}\NormalTok{):}
|
||
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist()}
|
||
\NormalTok{ wishlist.name }\OperatorTok{=}\NormalTok{ name}
|
||
\NormalTok{ wishlist.description }\OperatorTok{=}\NormalTok{ description}
|
||
\NormalTok{ wishlist.expiration\_date }\OperatorTok{=}\NormalTok{ expiration\_date}
|
||
\NormalTok{ wishlist.save()}
|
||
\ControlFlowTok{return}\NormalTok{ wishlist}
|
||
|
||
\KeywordTok{class}\NormalTok{ ChristmasWishlist(Wishlist):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ proxy }\OperatorTok{=} \VariableTok{True}
|
||
|
||
\AttributeTok{@staticmethod}
|
||
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):}
|
||
\NormalTok{ christmas }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{12}\NormalTok{, }\DecValTok{31}\NormalTok{)}
|
||
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, christmas)}
|
||
\NormalTok{ w.save()}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ EasterWishlist(Wishlist):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ proxy }\OperatorTok{=} \VariableTok{True}
|
||
|
||
\AttributeTok{@staticmethod}
|
||
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):}
|
||
\NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{4}\NormalTok{, }\DecValTok{1}\NormalTok{)}
|
||
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, expiration\_date)}
|
||
\NormalTok{ w.save()}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Gestion des utilisateurs
|
||
|
||
Dans les spécifications, nous souhaitions pouvoir associer un
|
||
utilisateur à une liste (\textbf{le propriétaire}) et un utilisateur à
|
||
une part (\textbf{le donateur}). Par défaut, Django offre une gestion
|
||
simplifiée des utilisateurs (pas de connexion LDAP, pas de double
|
||
authentification, \ldots\hspace{0pt}): juste un utilisateur et un mot de
|
||
passe. Pour y accéder, un paramètre par défaut est défini dans votre
|
||
fichier de settings: \texttt{AUTH\_USER\_MODEL}.
|
||
|
||
\hypertarget{_khana}{%
|
||
\section{Khana}\label{_khana}}
|
||
|
||
Khana est une application de suivi d'apprentissage pour des élèves ou
|
||
étudiants. Nous voulons pouvoir:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Lister les élèves
|
||
\item
|
||
Faire des listes de présence pour les élèves
|
||
\item
|
||
Pouvoir planifier ses cours
|
||
\item
|
||
Pouvoir suivre l'apprentissage des élèves, les liens qu'ils ont entre
|
||
les éléments à apprendre:
|
||
\item
|
||
pour écrire une phrase, il faut pouvoir écrire des mots, connaître la
|
||
grammaire, et connaître la conjugaison
|
||
\item
|
||
pour écrire des mots, il faut savoir écrire des lettres
|
||
\item
|
||
\ldots\hspace{0pt}
|
||
\end{enumerate}
|
||
|
||
Plusieurs professeurs s'occupent d'une même classe; il faut pouvoir
|
||
écrire des notes, envoyer des messages aux autres professeurs, etc.
|
||
|
||
Il faut également pouvoir définir des dates de contrôle, voir combien de
|
||
semaines il reste pour s'assurer d'avoir vu toute la matiètre.
|
||
|
||
Et pouvoir encoder les points des contrôles.
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/django-project-vs-apps-khana.png}
|
||
\caption{Khana}
|
||
\end{figure}
|
||
|
||
Unresolved directive in part-5-go-live/\_index.adoc -
|
||
include::legacy/\_main.adoc{[}{]}
|
||
|
||
\hypertarget{_glossaire}{%
|
||
\section{Glossaire}\label{_glossaire}}
|
||
|
||
\begin{description}
|
||
\item[http]
|
||
\emph{HyperText Transfer Protocol}, ou plus généralement le protocole
|
||
utilisé (et détourné) pour tout ce qui touche au \textbf{World Wide
|
||
Web}. Il existe beaucoup d'autres protocoles d'échange de données, comme
|
||
\href{https://fr.wikipedia.org/wiki/Gopher}{Gopher},
|
||
\href{https://fr.wikipedia.org/wiki/File_Transfer_Protocol}{FTP} ou
|
||
\href{https://fr.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol}{SMTP}.
|
||
\item[IaaS]
|
||
\emph{Infrastructure as a Service}, où un tiers vous fournit des
|
||
machines (généralement virtuelles) que vous devrez ensuite gérer en bon
|
||
père de famille. L'IaaS propose souvent une API, qui vous permet
|
||
d'intégrer la durée de vie de chaque machine dans vos flux - en créant,
|
||
augmentant, détruisant une machine lorsque cela s'avère nécessaire.
|
||
\item[MVC]
|
||
Le modèle \emph{Model-View-Controler} est un patron de conception
|
||
autorisant un faible couplage entre la gestion des données (le
|
||
\emph{Modèle}), l'affichage et le traitement de celles (la \emph{Vue})
|
||
et la glue entre ces deux composants (au travers du \emph{Contrôleur}).
|
||
\href{https://en.wikipedia.org/wiki/Model\%E2\%80\%93view\%E2\%80\%93controller}{Wikipédia}
|
||
\item[ORM]
|
||
\emph{Object Relational Mapper}, où une instance est directement (ou à
|
||
proximité) liée à un mode de persistance de données.
|
||
\item[PaaS]
|
||
\emph{Platform as a Service}, qui consiste à proposer les composants
|
||
d'une plateforme (Redis, PostgreSQL, \ldots\hspace{0pt}) en libre
|
||
service et disponibles à la demande (quoiqu'après avoir communiqué son
|
||
numéro de carte de crédit\ldots\hspace{0pt}).
|
||
\item[POO]
|
||
La \emph{Programmation Orientée Objet} est un paradigme de programmation
|
||
informatique. Elle consiste en la définition et l'interaction de briques
|
||
logicielles appelées objets ; un objet représente un concept, une idée
|
||
ou toute entité du monde physique, comme une voiture, une personne ou
|
||
encore une page d'un livre. Il possède une structure interne et un
|
||
comportement, et il sait interagir avec ses pairs. Il s'agit donc de
|
||
représenter ces objets et leurs relations ; l'interaction entre les
|
||
objets via leurs relations permet de concevoir et réaliser les
|
||
fonctionnalités attendues, de mieux résoudre le ou les problèmes. Dès
|
||
lors, l'étape de modélisation revêt une importance majeure et nécessaire
|
||
pour la POO. C'est elle qui permet de transcrire les éléments du réel
|
||
sous forme virtuelle.
|
||
\href{https://fr.wikipedia.org/wiki/Programmation_orient\%C3\%A9e_objet}{Wikipédia}
|
||
\item[S3]
|
||
Amazon \emph{Simple Storage Service} consiste en un système
|
||
d'hébergement de fichiers, quels qu'ils soient. Il peut s'agir de
|
||
fichiers de logs, de données applications, de fichiers média envoyés par
|
||
vos utilisateurs, de vidéos et images ou de données de sauvegardes.
|
||
\end{description}
|
||
|
||
\textbf{\url{https://aws.amazon.com/fr/s3/}.}
|
||
|
||
\includegraphics{images/amazon-s3-arch.png}
|
||
|
||
\hypertarget{_bibliographie}{%
|
||
\section{Bibliographie}\label{_bibliographie}}
|
||
|
||
bibliography::{[}{]}
|
||
|
||
\end{document}
|