gwift-book/asciidoc-to-tex.tex

3534 lines
144 KiB
TeX
Raw Blame History

This file contains invisible Unicode characters

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

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

\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}
\hypertarget{_structure_finale_de_notre_environnement}{%
\subsection{Structure finale de notre
environnement}\label{_structure_finale_de_notre_environnement}}
\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{_authentification}{%
\section{Authentification}\label{_authentification}}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ datetime}
\ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model}
\ImportTok{from}\NormalTok{ django.db.models }\ImportTok{import}\NormalTok{ Q}
\ImportTok{from}\NormalTok{ accounts.models }\ImportTok{import}\NormalTok{ Token }
\NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()}
\KeywordTok{class}\NormalTok{ TokenBackend(backends.ModelBackend):}
\KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):}
\CommentTok{"""Authentifie l\textquotesingle{}utilisateur sur base d\textquotesingle{}un jeton qu\textquotesingle{}il a reçu.}
\CommentTok{ On regarde la date de validité de chaque jeton avant d\textquotesingle{}autoriser l\textquotesingle{}accès.}
\CommentTok{ """}
\NormalTok{ token }\OperatorTok{=}\NormalTok{ kwargs.get(}\StringTok{"token"}\NormalTok{, }\VariableTok{None}\NormalTok{)}
\NormalTok{ current\_token }\OperatorTok{=}\NormalTok{ Token.objects.}\BuiltInTok{filter}\NormalTok{(token}\OperatorTok{=}\NormalTok{token, validity\_date\_\_gte}\OperatorTok{=}\NormalTok{datetime.now()).first()}
\ControlFlowTok{if}\NormalTok{ current\_token:}
\NormalTok{ user }\OperatorTok{=}\NormalTok{ current\_token.user}
\NormalTok{ current\_token.last\_used\_date }\OperatorTok{=}\NormalTok{ datetime.now()}
\NormalTok{ current\_token.save()}
\ControlFlowTok{return}\NormalTok{ user}
\ControlFlowTok{return} \VariableTok{None}
\end{Highlighting}
\end{Shaded}
\begin{itemize}
\item
Sous-entend qu'on a bien une classe qui permet d'accéder à ces jetons
;-)
\end{itemize}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model}
\ImportTok{from}\NormalTok{ ldap3 }\ImportTok{import}\NormalTok{ Server, Connection, ALL}
\ImportTok{from}\NormalTok{ ldap3.core.exceptions }\ImportTok{import}\NormalTok{ LDAPPasswordIsMandatoryError}
\ImportTok{from}\NormalTok{ config }\ImportTok{import}\NormalTok{ settings}
\NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()}
\KeywordTok{class}\NormalTok{ LdapBackend(backends.ModelBackend):}
\CommentTok{"""Implémentation du backend LDAP pour la connexion des utilisateurs à l\textquotesingle{}Active Directory.}
\CommentTok{ """}
\KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):}
\CommentTok{"""Authentifie l\textquotesingle{}utilisateur au travers du serveur LDAP.}
\CommentTok{ """}
\NormalTok{ ldap\_server }\OperatorTok{=}\NormalTok{ Server(settings.LDAP\_SERVER, get\_info}\OperatorTok{=}\NormalTok{ALL)}
\NormalTok{ ldap\_connection }\OperatorTok{=}\NormalTok{ Connection(ldap\_server, user}\OperatorTok{=}\NormalTok{username, password}\OperatorTok{=}\NormalTok{password)}
\ControlFlowTok{try}\NormalTok{:}
\ControlFlowTok{if} \KeywordTok{not}\NormalTok{ ldap\_connection.bind():}
\ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Login ou mot de passe incorrect"}\NormalTok{)}
\ControlFlowTok{except}\NormalTok{ (LDAPPasswordIsMandatoryError, }\PreprocessorTok{ValueError}\NormalTok{) }\ImportTok{as}\NormalTok{ ldap\_exception:}
\ControlFlowTok{raise}\NormalTok{ ldap\_exception}
\NormalTok{ user, \_ }\OperatorTok{=}\NormalTok{ UserModel.objects.get\_or\_create(username}\OperatorTok{=}\NormalTok{username)}
\end{Highlighting}
\end{Shaded}
On peut résumer le mécanisme d'authentification de la manière suivante:
\begin{itemize}
\item
Si vous voulez modifier les informations liées à un utilisateur,
orientez-vous vers la modification du modèle. Comme nous le verrons
ci-dessous, il existe trois manières de prendre ces modifications en
compte. Voir également
\href{https://docs.djangoproject.com/en/stable/topics/auth/customizing/}{ici}.
\item
Si vous souhaitez modifier la manière dont l'utilisateur se connecte,
alors vous devrez modifier le \textbf{backend}.
\end{itemize}
\hypertarget{_modification_du_moduxe8le}{%
\subsection{Modification du modèle}\label{_modification_du_moduxe8le}}
Dans un premier temps, Django a besoin de manipuler
\href{https://docs.djangoproject.com/en/1.9/ref/contrib/auth/\#user-model}{des
instances de type \texttt{django.contrib.auth.User}}. Cette classe
implémente les champs suivants:
\begin{itemize}
\item
\texttt{username}
\item
\texttt{first\_name}
\item
\texttt{last\_name}
\item
\texttt{email}
\item
\texttt{password}
\item
\texttt{date\_joined}.
\end{itemize}
D'autres champs, comme les groupes auxquels l'utilisateur est associé,
ses permissions, savoir s'il est un super-utilisateur,
\ldots\hspace{0pt} sont moins pertinents pour le moment. Avec les
quelques champs déjà définis ci-dessus, nous avons de quoi identifier
correctement nos utilisateurs. Inutile d'implémenter nos propres
classes, puisqu'elles existent déjà :-)
Si vous souhaitez ajouter un champ, il existe trois manières de faire.
\hypertarget{_extension_du_moduxe8le_existant}{%
\subsection{Extension du modèle
existant}\label{_extension_du_moduxe8le_existant}}
Le plus simple consiste à créer une nouvelle classe, et à faire un lien
de type \texttt{OneToOne} vers la classe
\texttt{django.contrib.auth.User}. De cette manière, on ne modifie rien
à la manière dont Django authentife ses utlisateurs: tout ce qu'on fait,
c'est un lien vers une table nouvellement créée, comme on l'a déjà vu au
point {[}\ldots\hspace{0pt}voir l'héritage de modèle{]}. L'avantage de
cette méthode, c'est qu'elle est extrêmement flexible, et qu'on garde
les mécanismes Django standard. Le désavantage, c'est que pour avoir
toutes les informations de notre utilisateur, on sera obligé d'effectuer
une jointure sur le base de données, ce qui pourrait avoir des
conséquences sur les performances.
\hypertarget{_substitution}{%
\subsection{Substitution}\label{_substitution}}
Avant de commencer, sachez que cette étape doit être effectuée
\textbf{avant la première migration}. Le plus simple sera de définir une
nouvelle classe héritant de \texttt{django.contrib.auth.User} et de
spécifier la classe à utiliser dans votre fichier de paramètres. Si ce
paramètre est modifié après que la première migration ait été effectuée,
il ne sera pas pris en compte. Tenez-en compte au moment de modéliser
votre application.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{AUTH\_USER\_MODEL }\OperatorTok{=} \StringTok{\textquotesingle{}myapp.MyUser\textquotesingle{}}
\end{Highlighting}
\end{Shaded}
Notez bien qu'il ne faut pas spécifier le package \texttt{.models} dans
cette injection de dépendances: le schéma à indiquer est bien
\texttt{\textless{}nom\ de\ lapplication\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}.
\hypertarget{_backend}{%
\subsubsection{Backend}\label{_backend}}
\hypertarget{_templates}{%
\subsubsection{Templates}\label{_templates}}
Ce qui n'existe pas par contre, ce sont les vues. Django propose donc
tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors
administration). En premier lieu, ces paramètres sont fixés dans le
fichier `settings
\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}.
On y trouve par exemple les paramètres suivants:
\begin{itemize}
\item
\texttt{LOGIN\_REDIRECT\_URL}: si vous ne spécifiez pas le paramètre
\texttt{next}, l'utilisateur sera automatiquement redirigé vers cette
page.
\item
\texttt{LOGIN\_URL}: l'URL de connexion à utiliser. Par défaut,
l'utilisateur doit se rendre sur la page \texttt{/accounts/login}.
\end{itemize}
\hypertarget{_social_authentification}{%
\subsubsection{Social-Authentification}\label{_social_authentification}}
Voir ici : \href{https://github.com/omab/python-social-auth}{python
social auth}
\hypertarget{_un_petit_mot_sur_oauth}{%
\subsubsection{Un petit mot sur OAuth}\label{_un_petit_mot_sur_oauth}}
OAuth est un standard libre définissant un ensemble de méthodes à
implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement
se base sur un système de jetons (Tokens), attribués par le possesseur
de la ressource à laquelle un utilisateur souhaite accéder.
Le client initie la connexion en demandant un jeton au serveur. Ce jeton
est ensuite utilisée tout au long de la connexion, pour accéder aux
différentes ressources offertes par ce serveur. `wikipedia
\textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}.
Une introduction à OAuth est
\href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}. Elle
introduit le protocole comme étant une \texttt{valet\ key}, une clé que
l'on donne à la personne qui va garer votre voiture pendant que vous
profitez des mondanités. Cette clé donne un accès à votre voiture, tout
en bloquant un ensemble de fonctionnalités. Le principe du protocole est
semblable en ce sens: vous vous réservez un accès total à une API,
tandis que le système de jetons permet d'identifier une personne, tout
en lui donnant un accès restreint à votre application.
L'utilisation de jetons permet notamment de définir une durée
d'utilisation et une portée d'utilisation. L'utilisateur d'un service A
peut par exemple autoriser un service B à accéder à des ressources qu'il
possède, sans pour autant révéler son nom d'utilisateur ou son mot de
passe.
L'exemple repris au niveau du
\href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le
suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site
faji.com (A). Elle souhaite les imprimer au travers du site beppa.com
(B). Au moment de la commande, le site beppa.com envoie une demande au
site faji.com pour accéder aux ressources partagées par Jane. Pour cela,
une nouvelle page s'ouvre pour l'utilisateur, et lui demande
d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de
B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui
permet d'y accéder.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
\StringTok{"django.contrib..."}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
peut être splitté en plusieurs parties:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
\NormalTok{]}
\NormalTok{THIRD\_PARTIES }\OperatorTok{=}\NormalTok{ [}
\NormalTok{]}
\NormalTok{MY\_APPS }\OperatorTok{=}\NormalTok{ [}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_context_processors}{%
\section{\texorpdfstring{\emph{Context
Processors}}{Context Processors}}\label{_context_processors}}
Mise en pratique: un \emph{context processor} sert \emph{grosso-modo} à
peupler l'ensemble des données transmises des vues aux templates avec
des données communes. Un context processor est un peu l'équivalent d'un
middleware, mais entre les données et les templates, là où le middleware
va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# core/context\_processors.py}
\ImportTok{import}\NormalTok{ subprocess}
\KeywordTok{def}\NormalTok{ git\_describe(request) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{str}\NormalTok{:}
\ControlFlowTok{return}\NormalTok{ \{}
\StringTok{"git\_describe"}\NormalTok{: subprocess.check\_output(}
\NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"describe"}\NormalTok{, }\StringTok{"{-}{-}always"}\NormalTok{]}
\NormalTok{ ).strip(),}
\StringTok{"git\_date"}\NormalTok{: subprocess.check\_output(}
\NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"show"}\NormalTok{, }\StringTok{"{-}s"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}format=}\SpecialCharTok{\%c}\VerbatimStringTok{d"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}date=format:}\SpecialCharTok{\%d}\VerbatimStringTok{{-}\%m{-}\%Y"}\NormalTok{]}
\NormalTok{ ),}
\NormalTok{ \}}
\end{Highlighting}
\end{Shaded}
Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe}
et \texttt{git\_date} dans tous les contextes de tous les templates de
l'application.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{TEMPLATES }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ \{}
\StringTok{\textquotesingle{}BACKEND\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}django.template.backends.django.DjangoTemplates\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}DIRS\textquotesingle{}}\NormalTok{: [os.path.join(BASE\_DIR, }\StringTok{"templates"}\NormalTok{),],}
\StringTok{\textquotesingle{}APP\_DIRS\textquotesingle{}}\NormalTok{: }\VariableTok{True}\NormalTok{,}
\StringTok{\textquotesingle{}OPTIONS\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}context\_processors\textquotesingle{}}\NormalTok{: [}
\StringTok{\textquotesingle{}django.template.context\_processors.debug\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.template.context\_processors.request\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.auth.context\_processors.auth\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.messages.context\_processors.messages\textquotesingle{}}\NormalTok{,}
\StringTok{"core.context\_processors.git\_describe"}
\NormalTok{ ],}
\NormalTok{ \},}
\NormalTok{ \},}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\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}
\end{document}