


\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\ l’application\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}.

\hypertarget{_backend}{%
\subsubsection{Backend}\label{_backend}}

\hypertarget{_templates}{%
\subsubsection{Templates}\label{_templates}}

Ce qui n'existe pas par contre, ce sont les vues. Django propose donc
tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors
administration). En premier lieu, ces paramètres sont fixés dans le
fichier `settings
\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}.
On y trouve par exemple les paramètres suivants:

\begin{itemize}
\item
  \texttt{LOGIN\_REDIRECT\_URL}: si vous ne spécifiez pas le paramètre
  \texttt{next}, l'utilisateur sera automatiquement redirigé vers cette
  page.
\item
  \texttt{LOGIN\_URL}: l'URL de connexion à utiliser. Par défaut,
  l'utilisateur doit se rendre sur la page \texttt{/accounts/login}.
\end{itemize}

\hypertarget{_social_authentification}{%
\subsubsection{Social-Authentification}\label{_social_authentification}}

Voir ici : \href{https://github.com/omab/python-social-auth}{python
social auth}

\hypertarget{_un_petit_mot_sur_oauth}{%
\subsubsection{Un petit mot sur OAuth}\label{_un_petit_mot_sur_oauth}}

OAuth est un standard libre définissant un ensemble de méthodes à
implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement
se base sur un système de jetons (Tokens), attribués par le possesseur
de la ressource à laquelle un utilisateur souhaite accéder.

Le client initie la connexion en demandant un jeton au serveur. Ce jeton
est ensuite utilisée tout au long de la connexion, pour accéder aux
différentes ressources offertes par ce serveur. `wikipedia
\textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}.

Une introduction à OAuth est
\href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}. Elle
introduit le protocole comme étant une \texttt{valet\ key}, une clé que
l'on donne à la personne qui va garer votre voiture pendant que vous
profitez des mondanités. Cette clé donne un accès à votre voiture, tout
en bloquant un ensemble de fonctionnalités. Le principe du protocole est
semblable en ce sens: vous vous réservez un accès total à une API,
tandis que le système de jetons permet d'identifier une personne, tout
en lui donnant un accès restreint à votre application.

L'utilisation de jetons permet notamment de définir une durée
d'utilisation et une portée d'utilisation. L'utilisateur d'un service A
peut par exemple autoriser un service B à accéder à des ressources qu'il
possède, sans pour autant révéler son nom d'utilisateur ou son mot de
passe.

L'exemple repris au niveau du
\href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le
suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site
faji.com (A). Elle souhaite les imprimer au travers du site beppa.com
(B). Au moment de la commande, le site beppa.com envoie une demande au
site faji.com pour accéder aux ressources partagées par Jane. Pour cela,
une nouvelle page s'ouvre pour l'utilisateur, et lui demande
d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de
B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui
permet d'y accéder.

\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
    \StringTok{"django.contrib..."}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}

peut être splitté en plusieurs parties:

\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}

\NormalTok{]}

\NormalTok{THIRD\_PARTIES }\OperatorTok{=}\NormalTok{ [}

\NormalTok{]}

\NormalTok{MY\_APPS }\OperatorTok{=}\NormalTok{ [}



\hypertarget{_logging_2}{%
\subsection{Logging}\label{_logging_2}}

\hypertarget{_muxe9thode_de_duxe9ploiement}{%
\section{Méthode de déploiement}\label{_muxe9thode_de_duxe9ploiement}}


\hypertarget{_duxe9ploiement_sur_debian}{%
\section{Déploiement sur Debian}\label{_duxe9ploiement_sur_debian}}


\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{_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}}



\end{document}
