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