\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{ [} \NormalTok{]} \end{Highlighting} \end{Shaded} \hypertarget{_context_processors}{% \section{\texorpdfstring{\emph{Context Processors}}{Context Processors}}\label{_context_processors}} Mise en pratique: un \emph{context processor} sert \emph{grosso-modo} à peupler l'ensemble des données transmises des vues aux templates avec des données communes. Un context processor est un peu l'équivalent d'un middleware, mais entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes. \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# core/context\_processors.py} \ImportTok{import}\NormalTok{ subprocess} \KeywordTok{def}\NormalTok{ git\_describe(request) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{str}\NormalTok{:} \ControlFlowTok{return}\NormalTok{ \{} \StringTok{"git\_describe"}\NormalTok{: subprocess.check\_output(} \NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"describe"}\NormalTok{, }\StringTok{"{-}{-}always"}\NormalTok{]} \NormalTok{ ).strip(),} \StringTok{"git\_date"}\NormalTok{: subprocess.check\_output(} \NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"show"}\NormalTok{, }\StringTok{"{-}s"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}format=}\SpecialCharTok{\%c}\VerbatimStringTok{d"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}date=format:}\SpecialCharTok{\%d}\VerbatimStringTok{{-}\%m{-}\%Y"}\NormalTok{]} \NormalTok{ ),} \NormalTok{ \}} \end{Highlighting} \end{Shaded} Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe} et \texttt{git\_date} dans tous les contextes de tous les templates de l'application. \begin{Shaded} \begin{Highlighting}[] \NormalTok{TEMPLATES }\OperatorTok{=}\NormalTok{ [} \NormalTok{ \{} \StringTok{\textquotesingle{}BACKEND\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}django.template.backends.django.DjangoTemplates\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}DIRS\textquotesingle{}}\NormalTok{: [os.path.join(BASE\_DIR, }\StringTok{"templates"}\NormalTok{),],} \StringTok{\textquotesingle{}APP\_DIRS\textquotesingle{}}\NormalTok{: }\VariableTok{True}\NormalTok{,} \StringTok{\textquotesingle{}OPTIONS\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}context\_processors\textquotesingle{}}\NormalTok{: [} \StringTok{\textquotesingle{}django.template.context\_processors.debug\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}django.template.context\_processors.request\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}django.contrib.auth.context\_processors.auth\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}django.contrib.messages.context\_processors.messages\textquotesingle{}}\NormalTok{,} \StringTok{"core.context\_processors.git\_describe"} \NormalTok{ ],} \NormalTok{ \},} \NormalTok{ \},} \NormalTok{]} \end{Highlighting} \end{Shaded} \begin{quote} To be effective, a software system must be deployable. The higher the cost of deployements, the less useful the system is. A goal of a software architecture, then, should be to make a system that can be easily deployed with a single action. Unfortunately, deployment strategy is seldom considered during initial development. This leads to architectures that may be make the system easy to develop, but leave it very difficult to deploy. --- Robert C. Martin Clean Architecture \end{quote} Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un serveur. Il est du coup probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles puissent être mises à disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé. Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque. L'objectif est qu'il soit rapide et fiable. Ceci peut être atteint au travers d'un partitionnement correct, incluant le fait que le composant principal s'assure que chaque sous-composant est correctement démarré intégré et supervisé. Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes. A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d'un tronc commun au développement cite:{[}devops\_handbook{]}. Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux versions), puis redémarrer les services adéquats, et la procédure complète se résumera à quelques lignes d'un script bash. \begin{quote} Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and disruptions such as service outages, service impairments, or security or compliance failures. --- DevOps Handbook Introduction \end{quote} Le serveur que django met à notre disposition \emph{via} la commande \texttt{runserver} est extrêmement pratique, mais il est uniquement prévu pour la phase développement: en production, il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, \ldots\hspace{0pt}). De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l'on connait ses limites et que l'on se limite à un utilisateur à la fois. En production, il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanés. En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données. En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou ou simplement parce que le serveur répondra trop lentement. L'objectif de cette partie est de parcourir les différentes possibilités qui s'offrent à nous en termes de déploiement, tout en faisant en sorte que le code soit le moins couplé possible à sa destination de production. L'objectif est donc de faire en sorte qu'une même application puisse être hébergées par plusieurs hôtes sans avoir à subir de modifications. Nous vous renvoyons vers les 12-facteurs dont nous avons déjà parlé et qui vous énormément nous aider, puisque ce sont des variables d'environnement qui vont réellement piloter le câblage entre l'application, ses composants et son hébergeur. RedHat proposait récemment un article intitulé \emph{*What Is IaaS*}, qui présentait les principales différences entre types d'hébergement. \begin{figure} \centering \includegraphics{images/deployment/iaas_focus-paas-saas-diagram.png} \caption{L'infrastructure en tant que service, cc. \emph{RedHat Cloud Computing}} \end{figure} Ainsi, on trouve: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Le déploiment \emph{on-premises} ou \emph{on-site} \item Les \emph{Infrastructures as a service} ou \emph{IaaSIaaS} \item Les \emph{Platforms as a service} ou \emph{PaaSPaaS} \item Les \emph{Softwares as a service} ou \emph{SaaSSaaS}, ce dernier point nous concernant moins, puisque c'est nous qui développons le logiciel. \end{enumerate} Dans cette partie, nous aborderons les points suivants: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Définir l'infrastructure et les composants nécessaires à notre application \item Configurer l'hôte qui hébergera l'application et y déployer notre application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt. A ce stade, nous aurons déjà une application disponible. \item Configurer les outils nécessaires à la bonne exécution de ce code et de ses fonctionnalités: les différentes méthodes de supervision de l'application, comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter correctement l'information. \end{enumerate} \hypertarget{_infrastructure_composants}{% \section{Infrastructure \& composants}\label{_infrastructure_composants}} Pour une mise ne production, le standard \emph{de facto} est le suivant: \begin{itemize} \item Nginx comme reverse proxy \item HAProxy pour la distribution de charge \item Gunicorn ou Uvicorn comme serveur d'application \item Supervisor pour le monitoring \item PostgreSQL ou MySQL/MariaDB comme bases de données. \item Celery et RabbitMQ pour l'exécution de tâches asynchrones \item Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier). \item Sentry, pour le suivi des bugs \end{itemize} Si nous schématisons l'infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item L'utilisateur fait une requête via son navigateur (Firefox ou Chrome) \item Le navigateur envoie une requête http, sa version, un verbe (GET, POST, \ldots\hspace{0pt}), un port et éventuellement du contenu \item Le firewall du serveur (Debian GNU/Linux, CentOS, \ldots\hspace{0pt}) vérifie si la requête peut être prise en compte \item La requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et \emph{a priori} Nginx) \item Elle est ensuite transmise par socket et est prise en compte par un des \emph{workers} (= un processus Python) instancié par Gunicorn. Si l'un de ces travailleurs venait à planter, il serait automatiquement réinstancié par Supervisord. \item Qui la transmet ensuite à l'un de ses \emph{workers} (= un processus Python). \item Après exécution, une réponse est renvoyée à l'utilisateur. \end{enumerate} \includegraphics{images/diagrams/architecture.png} \hypertarget{_reverse_proxy}{% \subsection{Reverse proxy}\label{_reverse_proxy}} Le principe du \textbf{proxy inverse} est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système. Il serait tout à fait possible de rendre notre application directement accessible depuis l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats \footnote{\url{https://fr.wikipedia.org/wiki/Proxy_inverse}} \hypertarget{_load_balancer}{% \subsection{Load balancer}\label{_load_balancer}} \hypertarget{_workers}{% \subsection{Workers}\label{_workers}} \hypertarget{_supervision_des_processus}{% \subsection{Supervision des processus}\label{_supervision_des_processus}} \hypertarget{_base_de_donnuxe9es_2}{% \subsection{Base de données}\label{_base_de_donnuxe9es_2}} \hypertarget{_tuxe2ches_asynchrones}{% \subsection{Tâches asynchrones}\label{_tuxe2ches_asynchrones}} \hypertarget{_mise_en_cache}{% \subsection{Mise en cache}\label{_mise_en_cache}} \hypertarget{_code_source}{% \section{Code source}\label{_code_source}} Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item effectuer le routage des données, \item trouver la bonne fonction à exécuter, \item récupérer les données depuis la base de données, \item effectuer le rendu ou la conversion des données, \item et renvoyer une réponse à l'utilisateur. \end{enumerate} Comme nous l'avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application. Il est possible de démarrer petit, et de suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, d'ajouter un mécanisme de mise en cache, des logiciels de suivi, \ldots\hspace{0pt} \hypertarget{_outils_de_supervision_et_de_mise_uxe0_disposition}{% \section{Outils de supervision et de mise à disposition}\label{_outils_de_supervision_et_de_mise_uxe0_disposition}} \hypertarget{_logs}{% \subsection{Logs}\label{_logs}} \hypertarget{_logging}{% \section{Logging}\label{_logging}} La structure des niveaux de journaux est essentielle. \begin{quote} When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m. Low printer toner is not an ERROR. --- Dan North former ToughtWorks consultant \end{quote} \begin{itemize} \item \textbf{DEBUG}: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application. Généralement, ce niveau est désactivé pour une application qui passe en production, sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement. \item \textbf{INFO}: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, \ldots\hspace{0pt} \item \textbf{WARN}: Regroupe les informations qui pourraient potentiellement devenir des erreurs. \item \textbf{ERROR}: Indique les informations internes - Erreur lors de l'appel d'une API, erreur interne, \ldots\hspace{0pt} \item \textbf{FATAL} (ou \textbf{EXCEPTION}): \ldots\hspace{0pt} généralement suivie d'une terminaison du programme ;-) - Bind raté d'un socket, etc. \end{itemize} La configuration des \emph{loggers} est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore. Il est ainsi possible de définir des formattages, gestionnaires (\emph{handlers}) et loggers distincts, en fonction de nos applications. Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements. Il n'est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les \texttt{StreamHandler}. La configuration que nous allons utiliser est celle-ci: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Formattage: à définir - mais la variante suivante est complète, lisible et pratique: \texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}} \item Handler: juste un, qui définit un \texttt{StreamHandler} \item Logger: pour celui-ci, nous avons besoin d'un niveau (\texttt{level}) et de savoir s'il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de \texttt{propagate} à \texttt{True}. \end{enumerate} \begin{Shaded} \begin{Highlighting}[] \NormalTok{LOGGING }\OperatorTok{=}\NormalTok{ \{} \StringTok{\textquotesingle{}version\textquotesingle{}}\NormalTok{: }\DecValTok{1}\NormalTok{,} \StringTok{\textquotesingle{}disable\_existing\_loggers\textquotesingle{}}\NormalTok{: }\VariableTok{False}\NormalTok{,} \StringTok{\textquotesingle{}formatters\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}verbose\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}format\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}}\SpecialCharTok{\{levelname\}}\StringTok{ }\SpecialCharTok{\{asctime\}}\StringTok{ }\SpecialCharTok{\{module\}}\StringTok{ }\SpecialCharTok{\{process:d\}}\StringTok{ }\SpecialCharTok{\{thread:d\}}\StringTok{ }\SpecialCharTok{\{message\}}\StringTok{\textquotesingle{}}\NormalTok{,} \NormalTok{ \},} \StringTok{\textquotesingle{}simple\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}format\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}}\SpecialCharTok{\{levelname\}}\StringTok{ }\SpecialCharTok{\{asctime\}}\StringTok{ }\SpecialCharTok{\{module\}}\StringTok{ }\SpecialCharTok{\{message\}}\StringTok{\textquotesingle{}}\NormalTok{,} \NormalTok{ \},} \NormalTok{ \},} \StringTok{\textquotesingle{}handlers\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}console\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}level\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}DEBUG\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}logging.StreamHandler\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}formatter\textquotesingle{}}\NormalTok{: }\StringTok{"verbose"} \NormalTok{ \}} \NormalTok{ \},} \StringTok{\textquotesingle{}loggers\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}khana\textquotesingle{}}\NormalTok{: \{} \StringTok{\textquotesingle{}handlers\textquotesingle{}}\NormalTok{: [}\StringTok{\textquotesingle{}console\textquotesingle{}}\NormalTok{],} \StringTok{\textquotesingle{}level\textquotesingle{}}\NormalTok{: env(}\StringTok{"LOG\_LEVEL"}\NormalTok{, default}\OperatorTok{=}\StringTok{"DEBUG"}\NormalTok{),} \StringTok{\textquotesingle{}propagate\textquotesingle{}}\NormalTok{: }\VariableTok{True}\NormalTok{,} \NormalTok{ \},} \NormalTok{ \}} \NormalTok{\}} \end{Highlighting} \end{Shaded} Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant: \begin{Shaded} \begin{Highlighting}[] \ImportTok{import}\NormalTok{ logging} \NormalTok{logger }\OperatorTok{=}\NormalTok{ logging.getLogger(}\VariableTok{\_\_name\_\_}\NormalTok{)} \NormalTok{logger.debug(}\StringTok{\textquotesingle{}helloworld\textquotesingle{}}\NormalTok{)} \end{Highlighting} \end{Shaded} \href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par exemples}. \hypertarget{_logging_2}{% \subsection{Logging}\label{_logging_2}} \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Sentry via sentry\_sdk \item Nagios \item LibreNMS \item Zabbix \end{enumerate} Il existe également \href{https://munin-monitoring.org}{Munin}, \href{https://www.elastic.co}{Logstash, ElasticSearch et Kibana (ELK-Stack)} ou \href{https://www.fluentd.org}{Fluentd}. \hypertarget{_muxe9thode_de_duxe9ploiement}{% \section{Méthode de déploiement}\label{_muxe9thode_de_duxe9ploiement}} Nous allons détailler ci-dessous trois méthodes de déploiement: \begin{itemize} \item Sur une machine hôte, en embarquant tous les composants sur un même serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de configurer un \emph{load balancer}, de routeur plusieurs basées de données, mais ce sera le premier cas de figure. \item Dans des containers, avec Docker-Compose. \item Sur une \textbf{Plateforme en tant que Service} (ou plus simplement, \textbf{PaaSPaaS}), pour faire abstraction de toute la couche de configuration du serveur. \end{itemize} \hypertarget{_duxe9ploiement_sur_debian}{% \section{Déploiement sur Debian}\label{_duxe9ploiement_sur_debian}} La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits. Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur \textbf{root}, car la moindre faille pourrait avoir des conséquences catastrophiques. Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'étape de configuration, qui consistera à: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Déployer les sources \item Démarrer un serveur implémentant une interface WSGI (\textbf{Web Server Gateway Interface}), qui sera chargé de créer autant de petits lutins travailleurs que nous le désirerons. \item Démarrer un superviseur, qui se chargera de veiller à la bonne santé de nos petits travailleurs, et en créer de nouveaux s'il le juge nécessaire \item Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes d'un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l'un des travailleurs. \end{enumerate} La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, \ldots\hspace{0pt} Il existe des dizaines d'hébergements typés VPS (\textbf{Virtual Private Server}). A vous de choisir celui qui vous convient \footnote{Personnellement, j'ai un petit faible pour Hetzner Cloud}. \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{apt}\NormalTok{ update} \ExtensionTok{groupadd}\NormalTok{ {-}{-}system webapps } \ExtensionTok{groupadd}\NormalTok{ {-}{-}system gunicorn\_sockets } \ExtensionTok{useradd}\NormalTok{ {-}{-}system {-}{-}gid webapps {-}{-}shell /bin/bash {-}{-}home /home/gwift gwift } \FunctionTok{mkdir}\NormalTok{ {-}p /home/gwift } \FunctionTok{chown}\NormalTok{ gwift:webapps /home/gwift } \end{Highlighting} \end{Shaded} \begin{itemize} \item On ajoute un groupe intitulé \texttt{webapps} \item On crée un groupe pour les communications via sockets \item On crée notre utilisateur applicatif; ses applications seront placées dans le répertoire \texttt{/home/gwift} \item On crée le répertoire home/gwift \item On donne les droits sur le répertoire /home/gwift \end{itemize} \hypertarget{_installation_des_duxe9pendances_systuxe8mes}{% \subsection{Installation des dépendances systèmes}\label{_installation_des_duxe9pendances_systuxe8mes}} La version 3.6 de Python se trouve dans les dépôts officiels de CentOS. Si vous souhaitez utiliser une version ultérieure, il suffit de l'installer en parallèle de la version officiellement supportée par votre distribution. Pour CentOS, vous avez donc deux possibilités : \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{yum}\NormalTok{ install python36 {-}y} \end{Highlighting} \end{Shaded} Ou passer par une installation alternative: \begin{Shaded} \begin{Highlighting}[] \FunctionTok{sudo}\NormalTok{ yum {-}y groupinstall }\StringTok{"Development Tools"} \FunctionTok{sudo}\NormalTok{ yum {-}y install openssl{-}devel bzip2{-}devel libffi{-}devel} \FunctionTok{wget}\NormalTok{ https://www.python.org/ftp/python/3.8.2/Python{-}3.8.2.tgz} \BuiltInTok{cd}\NormalTok{ Python{-}3.8*/} \ExtensionTok{./configure}\NormalTok{ {-}{-}enable{-}optimizations} \FunctionTok{sudo}\NormalTok{ make altinstall } \end{Highlighting} \end{Shaded} \begin{itemize} \item \textbf{Attention !} Le paramètre \texttt{altinstall} est primordial. Sans lui, vous écraserez l'interpréteur initialement supporté par la distribution, et cela pourrait avoir des effets de bord non souhaités. \end{itemize} \hypertarget{_installation_de_la_base_de_donnuxe9es}{% \subsection{Installation de la base de données}\label{_installation_de_la_base_de_donnuxe9es}} On l'a déjà vu, Django se base sur un pattern type \href{https://www.martinfowler.com/eaaCatalog/activeRecord.html}{ActiveRecords} pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données connus: \begin{itemize} \item SQLite (en natif, mais Django 3.0 exige une version du moteur supérieure ou égale à la 3.8) \item MariaDB (en natif depuis Django 3.0), \item PostgreSQL au travers de psycopg2 (en natif aussi), \item Microsoft SQLServer grâce aux drivers {[}\ldots\hspace{0pt}à compléter{]} \item Oracle via \href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}. \end{itemize} Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n'est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause d'un Oracle 12.1). Ci-dessous, quelques procédures d'installation pour mettre un serveur à disposition. Les deux plus simples seront MariaDB et PostgreSQL, qu'on couvrira ci-dessous. Oracle et Microsoft SQLServer se trouveront en annexes. \hypertarget{_postgresql}{% \subsubsection{PostgreSQL}\label{_postgresql}} On commence par installer PostgreSQL. Par exemple, dans le cas de debian, on exécute la commande suivante: \begin{Shaded} \begin{Highlighting}[] \VariableTok{$$}\NormalTok{$ }\ExtensionTok{aptitude}\NormalTok{ install postgresql postgresql{-}contrib} \end{Highlighting} \end{Shaded} Ensuite, on crée un utilisateur pour la DB: \begin{Shaded} \begin{Highlighting}[] \VariableTok{$$}\NormalTok{$ }\FunctionTok{su}\NormalTok{ {-} postgres} \ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ createuser {-}{-}interactive {-}P} \ExtensionTok{Enter}\NormalTok{ name of role to add: gwift\_user} \ExtensionTok{Enter}\NormalTok{ password for new role:} \ExtensionTok{Enter}\NormalTok{ it again:} \ExtensionTok{Shall}\NormalTok{ the new role be a superuser? (y/n) }\ExtensionTok{n} \ExtensionTok{Shall}\NormalTok{ the new role be allowed to create databases? (y/n) }\ExtensionTok{n} \ExtensionTok{Shall}\NormalTok{ the new role be allowed to create more new roles? (y/n) }\ExtensionTok{n} \ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$} \end{Highlighting} \end{Shaded} Finalement, on peut créer la DB: \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ createdb {-}{-}owner gwift\_user gwift} \ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ exit} \BuiltInTok{logout} \VariableTok{$$}\NormalTok{$} \end{Highlighting} \end{Shaded} penser à inclure un bidule pour les backups. \hypertarget{_mariadb}{% \subsubsection{MariaDB}\label{_mariadb}} Idem, installation, configuration, backup, tout ça. A copier de grimboite, je suis sûr d'avoir des notes là-dessus. \hypertarget{_microsoft_sql_server}{% \subsubsection{Microsoft SQL Server}\label{_microsoft_sql_server}} \hypertarget{_oracle}{% \subsubsection{Oracle}\label{_oracle}} \hypertarget{_pruxe9paration_de_lenvironnement_utilisateur}{% \subsection{Préparation de l'environnement utilisateur}\label{_pruxe9paration_de_lenvironnement_utilisateur}} \begin{Shaded} \begin{Highlighting}[] \FunctionTok{su}\NormalTok{ {-} gwift} \FunctionTok{cp}\NormalTok{ /etc/skel/.bashrc .} \FunctionTok{cp}\NormalTok{ /etc/skel/.bash\_profile .} \FunctionTok{ssh{-}keygen} \FunctionTok{mkdir}\NormalTok{ bin} \FunctionTok{mkdir}\NormalTok{ .venvs} \FunctionTok{mkdir}\NormalTok{ webapps} \ExtensionTok{python3.6}\NormalTok{ {-}m venv .venvs/gwift} \BuiltInTok{source}\NormalTok{ .venvs/gwift/bin/activate} \BuiltInTok{cd}\NormalTok{ /home/gwift/webapps} \FunctionTok{git}\NormalTok{ clone ...} \end{Highlighting} \end{Shaded} La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder. A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes suivantes: \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# en tant qu\textquotesingle{}utilisateur \textquotesingle{}gwift\textquotesingle{}} \BuiltInTok{source}\NormalTok{ .venvs/gwift/bin/activate} \ExtensionTok{pip}\NormalTok{ install {-}U pip} \ExtensionTok{pip}\NormalTok{ install {-}r requirements/base.txt} \ExtensionTok{pip}\NormalTok{ install gunicorn} \BuiltInTok{cd}\NormalTok{ webapps/gwift} \ExtensionTok{gunicorn}\NormalTok{ config.wsgi:application {-}{-}bind localhost:3000 {-}{-}settings=config.settings\_production} \end{Highlighting} \end{Shaded} \hypertarget{_configuration_de_lapplication}{% \subsection{Configuration de l'application}\label{_configuration_de_lapplication}} \begin{Shaded} \begin{Highlighting}[] \VariableTok{SECRET\_KEY=}\OperatorTok{\textless{}}\KeywordTok{set} \ExtensionTok{your}\NormalTok{ secret key here}\OperatorTok{\textgreater{}} \VariableTok{ALLOWED\_HOSTS=}\ExtensionTok{*} \VariableTok{STATIC\_ROOT=}\NormalTok{/var/www/gwift/static} \VariableTok{DATABASE=} \end{Highlighting} \end{Shaded} \begin{itemize} \item La variable \texttt{SECRET\_KEY} est notamment utilisée pour le chiffrement des sessions. \item On fait confiance à django\_environ pour traduire la chaîne de connexion à la base de données. \end{itemize} \hypertarget{_cruxe9ation_des_ruxe9pertoires_de_logs}{% \subsection{Création des répertoires de logs}\label{_cruxe9ation_des_ruxe9pertoires_de_logs}} \begin{verbatim} mkdir -p /var/www/gwift/static \end{verbatim} \hypertarget{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}{% \subsection{Création du répertoire pour le socket}\label{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}} Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}: \begin{verbatim} D /var/run/webapps 0775 gwift gunicorn_sockets - \end{verbatim} Suivi de la création par systemd : \begin{verbatim} systemd-tmpfiles --create \end{verbatim} \hypertarget{_gunicorn}{% \subsection{Gunicorn}\label{_gunicorn}} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\#!/bin/bash} \CommentTok{\# defines settings for gunicorn} \VariableTok{NAME=}\StringTok{"gwift"} \VariableTok{DJANGODIR=}\NormalTok{/home/gwift/webapps/gwift} \VariableTok{SOCKFILE=}\NormalTok{/var/run/webapps/gunicorn\_gwift.sock} \VariableTok{USER=}\NormalTok{gwift} \VariableTok{GROUP=}\NormalTok{gunicorn\_sockets} \VariableTok{NUM\_WORKERS=}\NormalTok{5} \VariableTok{DJANGO\_SETTINGS\_MODULE=}\NormalTok{config.settings\_production} \VariableTok{DJANGO\_WSGI\_MODULE=}\NormalTok{config.wsgi} \BuiltInTok{echo} \StringTok{"Starting }\VariableTok{$NAME}\StringTok{ as }\KeywordTok{\textasciigrave{}}\FunctionTok{whoami}\KeywordTok{\textasciigrave{}}\StringTok{"} \BuiltInTok{source}\NormalTok{ /home/gwift/.venvs/gwift/bin/activate} \BuiltInTok{cd} \VariableTok{$DJANGODIR} \BuiltInTok{export} \VariableTok{DJANGO\_SETTINGS\_MODULE=$DJANGO\_SETTINGS\_MODULE} \BuiltInTok{export} \VariableTok{PYTHONPATH=$DJANGODIR}\NormalTok{:}\VariableTok{$PYTHONPATH} \BuiltInTok{exec}\NormalTok{ gunicorn }\VariableTok{$\{DJANGO\_WSGI\_MODULE\}}\NormalTok{:application }\KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}name} \VariableTok{$NAME} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}workers} \VariableTok{$NUM\_WORKERS} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}user} \VariableTok{$USER} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}bind}\NormalTok{=unix:}\VariableTok{$SOCKFILE} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}log{-}level}\NormalTok{=debug }\KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}log{-}file}\NormalTok{={-}} \end{Highlighting} \end{Shaded} \hypertarget{_supervision_keep_alive_et_autoreload}{% \subsection{Supervision, keep-alive et autoreload}\label{_supervision_keep_alive_et_autoreload}} Pour la supervision, on passe par Supervisor. Il existe d'autres superviseurs, \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{yum}\NormalTok{ install supervisor {-}y} \end{Highlighting} \end{Shaded} On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}: \begin{Shaded} \begin{Highlighting}[] \NormalTok{[}\ExtensionTok{program}\NormalTok{:gwift]} \VariableTok{command=}\NormalTok{/home/gwift/bin/start\_gunicorn.sh} \VariableTok{user=}\NormalTok{gwift} \VariableTok{stdout\_logfile=}\NormalTok{/var/log/gwift/gwift.log} \VariableTok{autostart=}\NormalTok{true} \VariableTok{autorestart=}\NormalTok{unexpected} \VariableTok{redirect\_stdout=}\NormalTok{true} \VariableTok{redirect\_stderr=}\NormalTok{true} \end{Highlighting} \end{Shaded} Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu'il tourne correctement: \begin{Shaded} \begin{Highlighting}[] \FunctionTok{mkdir}\NormalTok{ /var/log/gwift} \FunctionTok{chown}\NormalTok{ gwift:nagios /var/log/gwift} \ExtensionTok{systemctl}\NormalTok{ enable supervisord} \ExtensionTok{systemctl}\NormalTok{ start supervisord.service} \ExtensionTok{systemctl}\NormalTok{ status supervisord.service} \NormalTok{● }\ExtensionTok{supervisord.service}\NormalTok{ {-} Process Monitoring and Control Daemon} \ExtensionTok{Loaded}\NormalTok{: loaded (/usr/lib/systemd/system/supervisord.service}\KeywordTok{;} \ExtensionTok{enabled}\KeywordTok{;} \ExtensionTok{vendor}\NormalTok{ preset: disabled)} \ExtensionTok{Active}\NormalTok{: active (running) }\ExtensionTok{since}\NormalTok{ Tue 2019{-}12{-}24 10:08:09 CET}\KeywordTok{;} \ExtensionTok{10s}\NormalTok{ ago} \ExtensionTok{Process}\NormalTok{: 2304 ExecStart=/usr/bin/supervisord {-}c /etc/supervisord.conf (code=exited, status=0/SUCCESS)} \ExtensionTok{Main}\NormalTok{ PID: 2310 (supervisord)} \ExtensionTok{CGroup}\NormalTok{: /system.slice/supervisord.service} \NormalTok{ ├─}\ExtensionTok{2310}\NormalTok{ /usr/bin/python /usr/bin/supervisord {-}c /etc/supervisord.conf} \NormalTok{ ├─}\ExtensionTok{2313}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2317}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2318}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2321}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2322}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ └─}\ExtensionTok{2323}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \FunctionTok{ls}\NormalTok{ /var/run/webapps/} \end{Highlighting} \end{Shaded} On peut aussi vérifier que l'application est en train de tourner, à l'aide de la commande \texttt{supervisorctl}: \begin{Shaded} \begin{Highlighting}[] \VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ status gwift} \ExtensionTok{gwift}\NormalTok{ RUNNING pid 31983, uptime 0:01:00} \end{Highlighting} \end{Shaded} Et pour gérer le démarrage ou l'arrêt, on peut passer par les commandes suivantes: \begin{Shaded} \begin{Highlighting}[] \VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ stop gwift} \ExtensionTok{gwift}\NormalTok{: stopped} \ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl start gwift} \ExtensionTok{gwift}\NormalTok{: started} \ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl restart gwift} \ExtensionTok{gwift}\NormalTok{: stopped} \ExtensionTok{gwift}\NormalTok{: started} \end{Highlighting} \end{Shaded} \hypertarget{_configuration_du_firewall_et_ouverture_des_ports}{% \subsection{Configuration du firewall et ouverture des ports}\label{_configuration_du_firewall_et_ouverture_des_ports}} \begin{verbatim} et 443 (HTTPS). \end{verbatim} \begin{verbatim} firewall-cmd --permanent --zone=public --add-service=http firewall-cmd --permanent --zone=public --add-service=https firewall-cmd --reload \end{verbatim} \begin{itemize} \item On ouvre le port 80, uniquement pour autoriser une connexion HTTP, mais qui sera immédiatement redirigée vers HTTPS \item Et le port 443 (forcément). \end{itemize} \hypertarget{_installation_dnginx}{% \subsection{Installation d'Nginx}\label{_installation_dnginx}} \begin{verbatim} yum install nginx -y usermod -a -G gunicorn_sockets nginx \end{verbatim} On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}: \begin{verbatim} upstream gwift_app { server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0; } server { listen 80; server_name ; root /var/www/gwift; error_log /var/log/nginx/gwift_error.log; access_log /var/log/nginx/gwift_access.log; client_max_body_size 4G; keepalive_timeout 5; gzip on; gzip_comp_level 7; gzip_proxied any; gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; location /static/ { access_log off; expires 30d; add_header Pragma public; add_header Cache-Control "public"; add_header Vary "Accept-Encoding"; try_files $uri $uri/ =404; } location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://gwift_app; } } \end{verbatim} \begin{itemize} \item Ce répertoire sera complété par la commande \texttt{collectstatic} que l'on verra plus tard. L'objectif est que les fichiers ne demandant aucune intelligence soit directement servis par Nginx. Cela évite d'avoir un processus Python (relativement lent) qui doive être instancié pour servir un simple fichier statique. \item Afin d'éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1 \end{itemize} \hypertarget{_mise_uxe0_jour}{% \subsection{Mise à jour}\label{_mise_uxe0_jour}} Script de mise à jour. \begin{Shaded} \begin{Highlighting}[] \FunctionTok{su}\NormalTok{ {-} }\OperatorTok{\textless{}}\NormalTok{user}\OperatorTok{\textgreater{}} \BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}}\NormalTok{/bin/activate} \BuiltInTok{cd}\NormalTok{ \textasciitilde{}/webapps/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}} \FunctionTok{git}\NormalTok{ fetch} \FunctionTok{git}\NormalTok{ checkout vX.Y.Z} \ExtensionTok{pip}\NormalTok{ install {-}U requirements/prod.txt} \ExtensionTok{python}\NormalTok{ manage.py migrate} \ExtensionTok{python}\NormalTok{ manage.py collectstatic} \BuiltInTok{kill}\NormalTok{ {-}HUP }\KeywordTok{\textasciigrave{}}\FunctionTok{ps}\NormalTok{ {-}C gunicorn fch {-}o pid }\KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}n 1}\KeywordTok{\textasciigrave{}} \end{Highlighting} \end{Shaded} \begin{itemize} \item \url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file} \end{itemize} \hypertarget{_configuration_des_sauvegardes}{% \subsection{Configuration des sauvegardes}\label{_configuration_des_sauvegardes}} Les sauvegardes ont été configurées avec borg: \texttt{yum\ install\ borgbackup}. C'est l'utilisateur gwift qui s'en occupe. \begin{verbatim} mkdir -p /home/gwift/borg-backups/ cd /home/gwift/borg-backups/ borg init gwift.borg -e=none borg create gwift.borg::{now} ~/bin ~/webapps \end{verbatim} Et dans le fichier crontab : \begin{verbatim} 0 23 * * * /home/gwift/bin/backup.sh \end{verbatim} \hypertarget{_rotation_des_jounaux}{% \subsection{Rotation des jounaux}\label{_rotation_des_jounaux}} \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{/var/log/gwift/*}\NormalTok{ \{} \ExtensionTok{weekly} \ExtensionTok{rotate}\NormalTok{ 3} \FunctionTok{size}\NormalTok{ 10M} \ExtensionTok{compress} \ExtensionTok{delaycompress} \NormalTok{\}} \end{Highlighting} \end{Shaded} Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift pour vérifier que cela fonctionne correctement. \hypertarget{_ansible}{% \subsection{Ansible}\label{_ansible}} TODO \hypertarget{_duxe9ploiement_sur_heroku}{% \section{Déploiement sur Heroku}\label{_duxe9ploiement_sur_heroku}} \href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A Service} paas, où vous choisissez le \emph{service} dont vous avez besoin (une base de données, un service de cache, un service applicatif, \ldots\hspace{0pt}), vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l'hôte. Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d'autant plus qu'il ne sera pas possible de modifier un fichier une fois qu'elle aura démarré: si vous souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et envoyer de nouveaux paramètres et recommencer le déploiement depuis le début. \begin{figure} \centering \includegraphics{images/deployment/heroku.png} \caption{Invest in apps, not ops. Heroku handles the hard stuff --- patching and upgrading, 24/7 ops and security, build systems, failovers, and more --- so your developers can stay focused on building great apps.} \end{figure} Pour un projet de type "hobby" et pour l'exemple de déploiement ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un \emph{Most Valuable Product} en place. La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un \emph{bucket compatible S3}, par exemple chez Amazon, Scaleway ou OVH. Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé. Il suffit donc d'envoyer les sources de votre application vers ce dépôt pour qu'Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition. Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l'équipe. Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de \emph{pipeline}, lorsque tous les tests unitaires et d'intégration auront été réalisés. Au travers de la commande \texttt{heroku\ create}, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier \texttt{.git/config} ci-dessous: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create} \ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098} \ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git} \NormalTok{$ }\FunctionTok{cat}\NormalTok{ .git/config} \NormalTok{[}\ExtensionTok{core}\NormalTok{]} \ExtensionTok{repositoryformatversion}\NormalTok{ = 0} \ExtensionTok{filemode}\NormalTok{ = false} \ExtensionTok{bare}\NormalTok{ = false} \ExtensionTok{logallrefupdates}\NormalTok{ = true} \ExtensionTok{symlinks}\NormalTok{ = false} \ExtensionTok{ignorecase}\NormalTok{ = true} \NormalTok{[}\ExtensionTok{remote} \StringTok{"heroku"}\NormalTok{]} \ExtensionTok{url}\NormalTok{ = https://git.heroku.com/still{-}thicket{-}66406.git} \ExtensionTok{fetch}\NormalTok{ = +refs/heads/*:refs/remotes/heroku/*} \end{Highlighting} \end{Shaded} IMPORTANT: \begin{verbatim} Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration. Celle-ci se limite aux deux fichiers suivants: * Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI * Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`) \end{verbatim} Après ce paramétrage, il suffit de pousser les changements vers ce nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}. Heroku propose des espaces de déploiements, mais pas d'espace de stockage. Il est possible d'y envoyer des fichiers utilisateurs (typiquement, des media personnalisés), mais ceux-ci seront perdus lors du redémarrage du container. Il est donc primordial de configurer correctement l'hébergement des fichiers média, de préférences sur un stockage compatible S3. s3 Prêt à vous lancer ? Commencez par créer un compte: \url{https://signup.heroku.com/python}. \hypertarget{_configuration_du_compte_heroku}{% \subsection{Configuration du compte Heroku}\label{_configuration_du_compte_heroku}} + Récupération des valeurs d'environnement pour les réutiliser ci-dessous. Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre première application; heureusement, la documentation est super bien faite: \begin{figure} \centering \includegraphics{images/deployment/heroku-new-app.png} \caption{Heroku: Commencer à travailler avec un langage} \end{figure} Installez ensuite la CLI (\emph{Command Line Interface}) en suivant \href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation suivante}. Au besoin, cette CLI existe pour: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item macOS, \emph{via} `brew ` \item Windows, grâce à un \href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la version 32 bits existe aussi, mais il est peu probable que vous en ayez besoin) \item GNU/Linux, via un script Shell \texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh} ou sur \href{https://snapcraft.io/heroku}{SnapCraft}. \end{enumerate} Une fois installée, connectez-vous: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ login} \end{Highlighting} \end{Shaded} Et créer votre application: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create} \ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098} \ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git} \end{Highlighting} \end{Shaded} \begin{figure} \centering \includegraphics{images/deployment/heroku-app-created.png} \caption{Notre application est à présent configurée!} \end{figure} Ajoutons lui une base de données, que nous sauvegarderons à intervalle régulier: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}postgresql:hobby{-}dev} \ExtensionTok{Creating}\NormalTok{ heroku{-}postgresql:hobby{-}dev on ⬢ still{-}thicket{-}66406... free} \ExtensionTok{Database}\NormalTok{ has been created and is available} \NormalTok{ ! }\ExtensionTok{This}\NormalTok{ database is empty. If upgrading, you can transfer} \NormalTok{ ! }\ExtensionTok{data}\NormalTok{ from another database with pg:copy} \ExtensionTok{Created}\NormalTok{ postgresql{-}clear{-}39693 as DATABASE\_URL} \ExtensionTok{Use}\NormalTok{ heroku addons:docs heroku{-}postgresql to view documentation} \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ pg:backups schedule {-}{-}at }\StringTok{\textquotesingle{}14:00 Europe/Brussels\textquotesingle{}}\NormalTok{ DATABASE\_URL} \ExtensionTok{Scheduling}\NormalTok{ automatic daily backups of postgresql{-}clear{-}39693 at 14:00 Europe/Brussels... done} \end{Highlighting} \end{Shaded} TODO: voir comment récupérer le backup de la db :-p \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# Copié/collé de https://cookiecutter{-}django.readthedocs.io/en/latest/deployment{-}on{-}heroku.html} \ExtensionTok{heroku}\NormalTok{ create {-}{-}buildpack https://github.com/heroku/heroku{-}buildpack{-}python} \ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}redis:hobby{-}dev} \ExtensionTok{heroku}\NormalTok{ addons:create mailgun:starter} \ExtensionTok{heroku}\NormalTok{ config:set PYTHONHASHSEED=random} \ExtensionTok{heroku}\NormalTok{ config:set WEB\_CONCURRENCY=4} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_DEBUG=False} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SETTINGS\_MODULE=config.settings.production} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SECRET\_KEY=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 64}\VariableTok{)}\StringTok{"} \CommentTok{\# Generating a 32 character{-}long random string without any of the visually similar characters "IOl01":} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ADMIN\_URL=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 4096 }\KeywordTok{|} \FunctionTok{tr}\NormalTok{ {-}dc }\StringTok{\textquotesingle{}A{-}HJ{-}NP{-}Za{-}km{-}z2{-}9\textquotesingle{}} \KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}c 32}\VariableTok{)}\StringTok{/"} \CommentTok{\# Set this to your Heroku app url, e.g. \textquotesingle{}bionic{-}beaver{-}28392.herokuapp.com\textquotesingle{}} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ALLOWED\_HOSTS=} \CommentTok{\# Assign with AWS\_ACCESS\_KEY\_ID} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_ACCESS\_KEY\_ID=} \CommentTok{\# Assign with AWS\_SECRET\_ACCESS\_KEY} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_SECRET\_ACCESS\_KEY=} \CommentTok{\# Assign with AWS\_STORAGE\_BUCKET\_NAME} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_STORAGE\_BUCKET\_NAME=} \FunctionTok{git}\NormalTok{ push heroku master} \ExtensionTok{heroku}\NormalTok{ run python manage.py createsuperuser} \ExtensionTok{heroku}\NormalTok{ run python manage.py check {-}{-}deploy} \ExtensionTok{heroku}\NormalTok{ open} \end{Highlighting} \end{Shaded} \hypertarget{_configuration}{% \subsection{Configuration}\label{_configuration}} Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement. Pour un projet Django, cela comprend, à placer à la racine de votre projet: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Un fichier \texttt{requirements.txt} (qui peut éventuellement faire appel à un autre fichier, \textbf{via} l'argument \texttt{-r}) \item Un fichier \texttt{Procfile} ({[}sans extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!), qui expliquera la commande pour le protocole WSGI. \end{enumerate} Dans notre exemple: \begin{verbatim} # requirements.txt django==3.2.8 gunicorn boto3 django-storages \end{verbatim} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# Procfile} \ExtensionTok{release}\NormalTok{: python3 manage.py migrate} \ExtensionTok{web}\NormalTok{: gunicorn gwift.wsgi} \end{Highlighting} \end{Shaded} \hypertarget{_huxe9bergement_s3}{% \subsection{Hébergement S3}\label{_huxe9bergement_s3}} Pour cette partie, nous allons nous baser sur l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de Scaleway}. Ils offrent 75GB de stockage et de transfert par mois, ce qui va nous laisser suffisament d'espace pour jouer un peu 😉. \includegraphics{images/deployment/scaleway-object-storage-bucket.png} L'idée est qu'au moment de la construction des fichiers statiques, Django aille simplement les héberger sur un espace de stockage compatible S3. La complexité va être de configurer correctement les différents points de terminaison. Pour héberger nos fichiers sur notre \textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes dans l'ordre: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y en a - \textbf{littéralement} - des dizaines. \item Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec ce type de protocole \item Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et Django, qui s'attend à ce qu'on lui donne un moteur de gestion \textbf{via} la clé {[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}). \end{enumerate} La première étape consiste à se rendre dans {[}la console Scaleway{]}(\url{https://console.scaleway.com/project/credentials}), pour gérer ses identifiants et créer un jeton. \includegraphics{images/deployment/scaleway-api-key.png} Selon la documentation de \href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages}, de \href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3} et de \href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway}, vous aurez besoin des clés suivantes au niveau du fichier \texttt{settings.py}: \begin{Shaded} \begin{Highlighting}[] \NormalTok{AWS\_ACCESS\_KEY\_ID }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}ACCESS\_KEY\_ID\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_SECRET\_ACCESS\_KEY }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}SECRET\_ACCESS\_KEY\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_STORAGE\_BUCKET\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_STORAGE\_BUCKET\_NAME\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_S3\_REGION\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_S3\_REGION\_NAME\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_DEFAULT\_ACL }\OperatorTok{=} \StringTok{\textquotesingle{}public{-}read\textquotesingle{}} \NormalTok{AWS\_LOCATION }\OperatorTok{=} \StringTok{\textquotesingle{}static\textquotesingle{}} \NormalTok{AWS\_S3\_SIGNATURE\_VERSION }\OperatorTok{=} \StringTok{\textquotesingle{}s3v4\textquotesingle{}} \NormalTok{AWS\_S3\_HOST }\OperatorTok{=} \StringTok{\textquotesingle{}s3.}\SpecialCharTok{\%s}\StringTok{.scw.cloud\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_REGION\_NAME,)} \NormalTok{AWS\_S3\_ENDPOINT\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}https://}\SpecialCharTok{\%s}\StringTok{\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_HOST, )} \NormalTok{DEFAULT\_FILE\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3Boto3Storage\textquotesingle{}} \NormalTok{STATICFILES\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3ManifestStaticStorage\textquotesingle{}} \NormalTok{STATIC\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}}\SpecialCharTok{\%s}\StringTok{/}\SpecialCharTok{\%s}\StringTok{/\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_ENDPOINT\_URL, AWS\_LOCATION)} \CommentTok{\# General optimization for faster delivery} \NormalTok{AWS\_IS\_GZIPPED }\OperatorTok{=} \VariableTok{True} \NormalTok{AWS\_S3\_OBJECT\_PARAMETERS }\OperatorTok{=}\NormalTok{ \{} \StringTok{\textquotesingle{}CacheControl\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}max{-}age=86400\textquotesingle{}}\NormalTok{,} \NormalTok{\}} \end{Highlighting} \end{Shaded} Configurez-les dans la console d'administration d'Heroku: \includegraphics{images/deployment/heroku-vars-reveal.png} Lors de la publication, vous devriez à présent avoir la sortie suivante, qui sera confirmée par le \textbf{bucket}: \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{remote}\NormalTok{: {-}{-}{-}{-}{-}}\OperatorTok{\textgreater{}}\NormalTok{ $ python manage.py collectstatic {-}{-}noinput} \ExtensionTok{remote}\NormalTok{: 128 static files copied, 156 post{-}processed.} \end{Highlighting} \end{Shaded} \includegraphics{images/deployment/gwift-cloud-s3.png} Sources complémentaires: \begin{itemize} \item {[}How to store Django static and media files on S3 in production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/}) \item {[}Using Django and Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/}) \end{itemize} \hypertarget{_docker_compose}{% \subsection{Docker-Compose}\label{_docker_compose}} (c/c Ced' - 2020-01-24) Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter pour django. D'abords, après avoir installer docker-compose et les dépendances sous debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root pour utiliser docker. Ensuite, j'ai relancé mon pc car juste relancé un shell n'a pas suffit pour que je puisse utiliser docker avec mon compte. Bon après c'est facile, un petit virtualenv pour cookiecutter, suivit d'une installation du template django. Et puis j'ai suivi sans t \url{https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html} Alors, il télécharge les images, fait un petit update, installe les dépendances de dev, install les requirement pip \ldots\hspace{0pt} Du coup, ça prend vite de la place: image.png L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque 1,5 GB d'un coup. Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien faire ou presque. La partie ci-dessous a été reprise telle quelle de \href{https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html}{la documentation de cookie-cutter-django}. le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt source. On peut aussi passer par fabric, ansible, chef ou puppet. \hypertarget{_autres_outils}{% \section{Autres outils}\label{_autres_outils}} Voir aussi devpi, circus, uswgi, statsd. See \url{https://mattsegal.dev/nginx-django-reverse-proxy-config.html} \hypertarget{_ressources}{% \section{Ressources}\label{_ressources}} \begin{itemize} \item \url{https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/} \item \href{https://docs.djangoproject.com/fr/3.0/howto/deployment/}{Déploiement}. \item Let's Encrypt ! \end{itemize} Nous avons fait exprès de reprendre l'acronyme d'une \emph{Services Oriented Architecture} pour cette partie. L'objectif est de vous mettre la puce à l'oreille quant à la finalité du développement: que l'utilisateur soit humain, bot automatique ou client Web, l'objectif est de fournir des applications résilientes, disponibles et accessibles. Dans cette partie, nous aborderons les vues, la mise en forme, la mise en page, la définition d'une interface REST, la définition d'une interface GraphQL et le routage d'URLs. \hypertarget{_application_programming_interface}{% \section{Application Programming Interface}\label{_application_programming_interface}} \url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs Django Rest Framework Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis. Voir peut-être aussi \url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/} Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service d'affectation. Quelque chose comme ceci: \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# models.py} \ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ models} \KeywordTok{class}\NormalTok{ People(models.Model):} \NormalTok{ CIVILITY\_CHOICES }\OperatorTok{=}\NormalTok{ (} \NormalTok{ (}\StringTok{"M"}\NormalTok{, }\StringTok{"Monsieur"}\NormalTok{),} \NormalTok{ (}\StringTok{"Mme"}\NormalTok{, }\StringTok{"Madame"}\NormalTok{),} \NormalTok{ (}\StringTok{"Dr"}\NormalTok{, }\StringTok{"Docteur"}\NormalTok{),} \NormalTok{ (}\StringTok{"Pr"}\NormalTok{, }\StringTok{"Professeur"}\NormalTok{),} \NormalTok{ (}\StringTok{""}\NormalTok{, }\StringTok{""}\NormalTok{)} \NormalTok{ )} \NormalTok{ last\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} \NormalTok{ first\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} \NormalTok{ civility }\OperatorTok{=}\NormalTok{ models.CharField(} \NormalTok{ max\_length}\OperatorTok{=}\DecValTok{3}\NormalTok{,} \NormalTok{ choices}\OperatorTok{=}\NormalTok{CIVILITY\_CHOICES,} \NormalTok{ default}\OperatorTok{=}\StringTok{""} \NormalTok{ )} \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} \ControlFlowTok{return} \StringTok{"}\SpecialCharTok{\{\}}\StringTok{, }\SpecialCharTok{\{\}}\StringTok{"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}\VariableTok{self}\NormalTok{.last\_name, }\VariableTok{self}\NormalTok{.first\_name)} \KeywordTok{class}\NormalTok{ Service(models.Model):} \NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} \ControlFlowTok{return} \VariableTok{self}\NormalTok{.label} \KeywordTok{class}\NormalTok{ ContractType(models.Model):} \NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} \NormalTok{ short\_label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)} \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} \ControlFlowTok{return} \VariableTok{self}\NormalTok{.short\_label} \KeywordTok{class}\NormalTok{ Contract(models.Model):} \NormalTok{ people }\OperatorTok{=}\NormalTok{ models.ForeignKey(People, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)} \NormalTok{ date\_begin }\OperatorTok{=}\NormalTok{ models.DateField()} \NormalTok{ date\_end }\OperatorTok{=}\NormalTok{ models.DateField(blank}\OperatorTok{=}\VariableTok{True}\NormalTok{, null}\OperatorTok{=}\VariableTok{True}\NormalTok{)} \NormalTok{ contract\_type }\OperatorTok{=}\NormalTok{ models.ForeignKey(ContractType, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)} \NormalTok{ service }\OperatorTok{=}\NormalTok{ models.ForeignKey(Service, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)} \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} \ControlFlowTok{if} \VariableTok{self}\NormalTok{.date\_end }\KeywordTok{is} \KeywordTok{not} \VariableTok{None}\NormalTok{:} \ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, jusqu\textquotesingle{}au }\SpecialCharTok{\{\}}\StringTok{, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(} \VariableTok{self}\NormalTok{.date\_begin,} \VariableTok{self}\NormalTok{.date\_end,} \VariableTok{self}\NormalTok{.service,} \VariableTok{self}\NormalTok{.contract\_type} \NormalTok{ )} \ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, à durée indéterminée, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(} \VariableTok{self}\NormalTok{.date\_begin,} \VariableTok{self}\NormalTok{.service,} \VariableTok{self}\NormalTok{.contract\_type} \NormalTok{ )} \end{Highlighting} \end{Shaded} \includegraphics{images/rest/models.png} \hypertarget{_configuration_2}{% \section{Configuration}\label{_configuration_2}} La configuration des points de terminaison de notre API est relativement touffue. Il convient de: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de l'API, \item Configurer les vues, càd le comportement de chacun des points de terminaison, \item Configurer les points de terminaison eux-mêmes, càd les URLs permettant d'accéder aux ressources. \item Et finalement ajouter quelques paramètres au niveau de notre application. \end{enumerate} \hypertarget{_suxe9rialiseurs}{% \subsection{Sérialiseurs}\label{_suxe9rialiseurs}} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# serializers.py} \ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group} \ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ serializers} \ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service} \KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ model }\OperatorTok{=}\NormalTok{ People} \NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)} \KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract} \NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)} \KeywordTok{class}\NormalTok{ ServiceSerializer(serializers.HyperlinkedModelSerializer):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ model }\OperatorTok{=}\NormalTok{ Service} \NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"name"}\NormalTok{,)} \end{Highlighting} \end{Shaded} \hypertarget{_vues}{% \subsection{Vues}\label{_vues}} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# views.py} \ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group} \ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets} \ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ permissions} \ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service} \ImportTok{from}\NormalTok{ .serializers }\ImportTok{import}\NormalTok{ PeopleSerializer, ContractSerializer, ServiceSerializer} \KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):} \NormalTok{ queryset }\OperatorTok{=}\NormalTok{ People.objects.}\BuiltInTok{all}\NormalTok{()} \NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ PeopleSerializer} \NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]} \KeywordTok{class}\NormalTok{ ContractViewSet(viewsets.ModelViewSet):} \NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Contract.objects.}\BuiltInTok{all}\NormalTok{()} \NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ContractSerializer} \NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]} \KeywordTok{class}\NormalTok{ ServiceViewSet(viewsets.ModelViewSet):} \NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Service.objects.}\BuiltInTok{all}\NormalTok{()} \NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ServiceSerializer} \NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]} \end{Highlighting} \end{Shaded} \hypertarget{_urls}{% \subsection{URLs}\label{_urls}} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# urls.py} \ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} \ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path, include} \ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ routers} \ImportTok{from}\NormalTok{ core }\ImportTok{import}\NormalTok{ views} \NormalTok{router }\OperatorTok{=}\NormalTok{ routers.DefaultRouter()} \NormalTok{router.register(}\VerbatimStringTok{r"people"}\NormalTok{, views.PeopleViewSet)} \NormalTok{router.register(}\VerbatimStringTok{r"contracts"}\NormalTok{, views.ContractViewSet)} \NormalTok{router.register(}\VerbatimStringTok{r"services"}\NormalTok{, views.ServiceViewSet)} \NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} \NormalTok{ path(}\StringTok{"api/v1/"}\NormalTok{, include(router.urls)),} \NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls),} \NormalTok{]} \end{Highlighting} \end{Shaded} \hypertarget{_paramuxe8tres}{% \subsection{Paramètres}\label{_paramuxe8tres}} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# settings.py} \NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [} \NormalTok{ ...} \StringTok{"rest\_framework"}\NormalTok{,} \NormalTok{ ...} \NormalTok{]} \NormalTok{...} \NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{} \StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10} \NormalTok{\}} \end{Highlighting} \end{Shaded} A ce stade, en nous rendant sur l'URL \texttt{http://localhost:8000/api/v1}, nous obtiendrons ceci: \includegraphics{images/rest/api-first-example.png} \hypertarget{_moduxe8les_et_relations}{% \section{Modèles et relations}\label{_moduxe8les_et_relations}} Plus haut, nous avons utilisé une relation de type \texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité. Pour palier à ceci, il existe {[}plusieurs manières de représenter ces relations{]}(\url{https://www.django-rest-framework.org/api-guide/relations/}): soit \textbf{via} un hyperlien, comme ci-dessus, soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource. La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats): \begin{Shaded} \begin{Highlighting}[] \FunctionTok{\{} \DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,} \DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} \DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} \DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[} \FunctionTok{\{} \DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,} \DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,} \DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[} \StringTok{"http://localhost:8000/api/v1/contracts/1/"}\OtherTok{,} \StringTok{"http://localhost:8000/api/v1/contracts/2/"} \OtherTok{]} \FunctionTok{\}} \OtherTok{]} \FunctionTok{\}} \end{Highlighting} \end{Shaded} à ceci: \begin{Shaded} \begin{Highlighting}[] \FunctionTok{\{} \DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,} \DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} \DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} \DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[} \FunctionTok{\{} \DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,} \DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,} \DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[} \FunctionTok{\{} \DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2019{-}01{-}01"}\FunctionTok{,} \DataTypeTok{"date\_end"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} \DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"} \FunctionTok{\}}\OtherTok{,} \FunctionTok{\{} \DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2009{-}01{-}01"}\FunctionTok{,} \DataTypeTok{"date\_end"}\FunctionTok{:} \StringTok{"2021{-}01{-}01"}\FunctionTok{,} \DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"} \FunctionTok{\}} \OtherTok{]} \FunctionTok{\}} \OtherTok{]} \FunctionTok{\}} \end{Highlighting} \end{Shaded} La modification se limite à \textbf{surcharger} la propriété, pour indiquer qu'elle consiste en une instance d'un des sérialiseurs existants. Nous passons ainsi de ceci \begin{Shaded} \begin{Highlighting}[] \KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract} \NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)} \KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ model }\OperatorTok{=}\NormalTok{ People} \NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)} \end{Highlighting} \end{Shaded} à ceci: \begin{Shaded} \begin{Highlighting}[] \KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract} \NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)} \KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):} \NormalTok{ contract\_set }\OperatorTok{=}\NormalTok{ ContractSerializer(many}\OperatorTok{=}\VariableTok{True}\NormalTok{, read\_only}\OperatorTok{=}\VariableTok{True}\NormalTok{)} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ model }\OperatorTok{=}\NormalTok{ People} \NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)} \end{Highlighting} \end{Shaded} Nous ne faisons donc bien que redéfinir la propriété \texttt{contract\_set} et indiquons qu'il s'agit à présent d'une instance de \texttt{ContractSerializer}, et qu'il est possible d'en avoir plusieurs. C'est tout. \hypertarget{_filtres_et_recherches}{% \section{Filtres et recherches}\label{_filtres_et_recherches}} A ce stade, nous pouvons juste récupérer des informations présentes dans notre base de données, mais à part les parcourir, il est difficile d'en faire quelque chose. Il est possible de jouer avec les URLs en définissant une nouvelle route ou avec les paramètres de l'URL, ce qui demanderait alors de programmer chaque cas possible - sans que le consommateur ne puisse les déduire lui-même. Une solution élégante consiste à autoriser le consommateur à filtrer les données, directement au niveau de l'API. Ceci peut être fait. Il existe deux manières de restreindre l'ensemble des résultats retournés: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Soit au travers d'une recherche, qui permet d'effectuer une recherche textuelle, globale et par ensemble à un ensemble de champs, \item Soit au travers d'un filtre, ce qui permet de spécifier une valeur précise à rechercher. \end{enumerate} Dans notre exemple, la première possibilité sera utile pour rechercher une personne répondant à un ensemble de critères. Typiquement, \texttt{/api/v1/people/?search=raymond\ bond} ne nous donnera aucun résultat, alors que \texttt{/api/v1/people/?search=james\ bond} nous donnera le célèbre agent secret (qui a bien entendu un contrat chez nous\ldots\hspace{0pt}). Le second cas permettra par contre de préciser que nous souhaitons disposer de toutes les personnes dont le contrat est ultérieur à une date particulière. Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de proposer immédiatement les champs, et donc d'informer le consommateur des possibilités: \includegraphics{images/rest/drf-filters-and-searches.png} \hypertarget{_recherches}{% \subsection{Recherches}\label{_recherches}} La fonction de recherche est déjà implémentée au niveau de Django-Rest-Framework, et aucune dépendance supplémentaire n'est nécessaire. Au niveau du \texttt{viewset}, il suffit d'ajouter deux informations: \begin{Shaded} \begin{Highlighting}[] \NormalTok{...} \ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ filters, viewsets} \NormalTok{...} \KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):} \NormalTok{ ...} \NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [filters.SearchFilter]} \NormalTok{ search\_fields }\OperatorTok{=}\NormalTok{ [}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{]} \NormalTok{ ...} \end{Highlighting} \end{Shaded} \hypertarget{_filtres}{% \subsection{Filtres}\label{_filtres}} Nous commençons par installer {[}le paquet \texttt{django-filter}{]}(\url{https://www.django-rest-framework.org/api-guide/filtering/\#djangofilterbackend}) et nous l'ajoutons parmi les applications installées: \begin{Shaded} \begin{Highlighting}[] \NormalTok{λ }\ExtensionTok{pip}\NormalTok{ install django{-}filter} \ExtensionTok{Collecting}\NormalTok{ django{-}filter} \ExtensionTok{Downloading}\NormalTok{ django\_filter{-}2.4.0{-}py3{-}none{-}any.whl (73 kB)} \KeywordTok{|}\NormalTok{████████████████████████████████}\KeywordTok{|} \ExtensionTok{73}\NormalTok{ kB 2.6 MB/s} \ExtensionTok{Requirement}\NormalTok{ already satisfied: Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from django{-}filter) }\KeywordTok{(}\ExtensionTok{3.1.7}\KeywordTok{)} \ExtensionTok{Requirement}\NormalTok{ already satisfied: asgiref}\OperatorTok{\textless{}}\NormalTok{4,}\OperatorTok{\textgreater{}}\NormalTok{=3.2.10 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{3.3.1}\KeywordTok{)} \ExtensionTok{Requirement}\NormalTok{ already satisfied: sqlparse}\OperatorTok{\textgreater{}}\NormalTok{=0.2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{0.4.1}\KeywordTok{)} \ExtensionTok{Requirement}\NormalTok{ already satisfied: pytz in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{2021.1}\KeywordTok{)} \ExtensionTok{Installing}\NormalTok{ collected packages: django{-}filter} \ExtensionTok{Successfully}\NormalTok{ installed django{-}filter{-}2.4.0} \end{Highlighting} \end{Shaded} Une fois l'installée réalisée, il reste deux choses à faire: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Ajouter \texttt{django\_filters} parmi les applications installées: \item Configurer la clé \texttt{DEFAULT\_FILTER\_BACKENDS} à la valeur \texttt{{[}\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}{]}}. \end{enumerate} Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier \texttt{settings.py} de la manière suivante: \begin{Shaded} \begin{Highlighting}[] \NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{} \StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,} \StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10}\NormalTok{,} \StringTok{\textquotesingle{}DEFAULT\_FILTER\_BACKENDS\textquotesingle{}}\NormalTok{: [}\StringTok{\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}}\NormalTok{]} \NormalTok{\}} \end{Highlighting} \end{Shaded} Au niveau du viewset, il convient d'ajouter ceci: \begin{Shaded} \begin{Highlighting}[] \NormalTok{...} \ImportTok{from}\NormalTok{ django\_filters.rest\_framework }\ImportTok{import}\NormalTok{ DjangoFilterBackend} \ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets} \NormalTok{...} \KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):} \NormalTok{ ...} \NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [DjangoFilterBackend]} \NormalTok{ filterset\_fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}last\_name\textquotesingle{}}\NormalTok{,)} \NormalTok{ ...} \end{Highlighting} \end{Shaded} A ce stade, nous avons deux problèmes: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Le champ que nous avons défini au niveau de la propriété \texttt{filterset\_fields} exige une correspondance exacte. Ainsi, \texttt{/api/v1/people/?last\_name=Bon} ne retourne rien, alors que \texttt{/api/v1/people/?last\_name=Bond} nous donnera notre agent secret préféré. \item Il n'est pas possible d'aller appliquer un critère de sélection sur la propriété d'une relation. Notre exemple proposant rechercher uniquement les relations dans le futur (ou dans le passé) tombe à l'eau. \end{enumerate} Pour ces deux points, nous allons définir un nouveau filtre, en surchargeant une nouvelle classe dont la classe mère serait de type \texttt{django\_filters.FilterSet}. TO BE CONTINUED. A noter qu'il existe un paquet {[}Django-Rest-Framework-filters{]}(\url{https://github.com/philipn/django-rest-framework-filters}), mais il est déprécié depuis Django 3.0, puisqu'il se base sur \texttt{django.utils.six} qui n'existe à présent plus. Il faut donc le faire à la main (ou patcher le paquet\ldots\hspace{0pt}). \hypertarget{_urls_et_espaces_de_noms}{% \section{URLs et espaces de noms}\label{_urls_et_espaces_de_noms}} La gestion des URLs permet \textbf{grosso modo} d'assigner une adresse paramétrée ou non à une fonction Python. La manière simple consiste à modifier le fichier \texttt{gwift/settings.py} pour y ajouter nos correspondances. Par défaut, le fichier ressemble à ceci: \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# gwift/urls.py} \ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url} \ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} \NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} \NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),} \NormalTok{]} \end{Highlighting} \end{Shaded} La variable \texttt{urlpatterns} associe un ensemble d'adresses à des fonctions. Dans le fichier \textbf{nu}, seul le \textbf{pattern} \texttt{admin} est défini, et inclut toutes les adresses qui sont définies dans le fichier \texttt{admin.site.urls}. Django fonctionne avec des \textbf{expressions rationnelles} simplifiées (des \textbf{expressions régulières} ou \textbf{regex}) pour trouver une correspondance entre une URL et la fonction qui recevra la requête et retournera une réponse. Nous utilisons l'expression \texttt{\^{}\$} pour déterminer la racine de notre application, mais nous pourrions appliquer d'autres regroupements (\texttt{/home}, \texttt{users/\textless{}profile\_id\textgreater{}}, \texttt{articles/\textless{}year\textgreater{}/\textless{}month\textgreater{}/\textless{}day\textgreater{}}, \ldots\hspace{0pt}). Chaque \textbf{variable} déclarée dans l'expression régulière sera apparenté à un paramètre dans la fonction correspondante. Ainsi, \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# admin.site.urls.py} \end{Highlighting} \end{Shaded} Pour reprendre l'exemple où on en était resté: \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# gwift/urls.py} \ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url} \ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} \ImportTok{from}\NormalTok{ wish }\ImportTok{import}\NormalTok{ views }\ImportTok{as}\NormalTok{ wish\_views} \NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} \NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),} \NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, wish\_views.wishlists, name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),} \NormalTok{]} \end{Highlighting} \end{Shaded} Dans la mesure du possible, essayez toujours de \textbf{nommer} chaque expression. Cela permettra notamment de les retrouver au travers de la fonction \texttt{reverse}, mais permettra également de simplifier vos templates. A présent, on doit tester que l'URL racine de notre application mène bien vers la fonction \texttt{wish\_views.wishlists}. Sauf que les pages \texttt{about} et \texttt{help} existent également. Pour implémenter ce type de précédence, il faudrait implémenter les URLs de la manière suivante: \begin{verbatim} | about | help | \end{verbatim} Mais cela signifie aussi que les utilisateurs \texttt{about} et \texttt{help} (s'ils existent\ldots\hspace{0pt}) ne pourront jamais accéder à leur profil. Une dernière solution serait de maintenir une liste d'authorité des noms d'utilisateur qu'il n'est pas possible d'utiliser. D'où l'importance de bien définir la séquence de déinition de ces routes, ainsi que des espaces de noms. Note sur les namespaces. De là, découle une autre bonne pratique: l'utilisation de \emph{breadcrumbs} (\url{https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template}) ou de guidelines de navigation. \hypertarget{_reverse}{% \subsection{Reverse}\label{_reverse}} En associant un nom ou un libellé à chaque URL, il est possible de récupérer sa \textbf{traduction}. Cela implique par contre de ne plus toucher à ce libellé par la suite\ldots\hspace{0pt} Dans le fichier \texttt{urls.py}, on associe le libellé \texttt{wishlists} à l'URL \texttt{r\textquotesingle{}\^{}\$} (c'est-à-dire la racine du site): \begin{Shaded} \begin{Highlighting}[] \ImportTok{from}\NormalTok{ wish.views }\ImportTok{import}\NormalTok{ WishListList} \NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} \NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),} \NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, WishListList.as\_view(), name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),} \NormalTok{]} \end{Highlighting} \end{Shaded} De cette manière, dans nos templates, on peut à présent construire un lien vers la racine avec le tags suivant: \begin{Shaded} \begin{Highlighting}[] \KeywordTok{\textless{}a}\OtherTok{ href=}\StringTok{"\{\% url \textquotesingle{}wishlists\textquotesingle{} \%\}"}\KeywordTok{\textgreater{}}\NormalTok{\{\{ yearvar \}\} Archive}\KeywordTok{\textless{}/a\textgreater{}} \end{Highlighting} \end{Shaded} De la même manière, on peut également récupérer l'URL de destination pour n'importe quel libellé, de la manière suivante: \begin{Shaded} \begin{Highlighting}[] \ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse\_lazy} \NormalTok{wishlists\_url }\OperatorTok{=}\NormalTok{ reverse\_lazy(}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{)} \end{Highlighting} \end{Shaded} \hypertarget{_i18n_l10n}{% \section{i18n / l10n}\label{_i18n_l10n}} La localisation (\emph{l10n}) et l'internationalization (\emph{i18n}) sont deux concepts proches, mais différents: \begin{itemize} \item Internationalisation: \emph{Preparing the software for localization. Usually done by developers.} \item Localisation: \emph{Writing the translations and local formats. Usually done by translators.} \end{itemize} L'internationalisation est donc le processus permettant à une application d'accepter une forme de localisation. La seconde ne va donc pas sans la première, tandis que la première ne fait qu'autoriser la seconde. \hypertarget{_arborescences}{% \subsection{Arborescences}\label{_arborescences}} \begin{Shaded} \begin{Highlighting}[] \end{Highlighting} \end{Shaded} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# \textless{}app\textgreater{}/management/commands/rebuild.py} \CommentTok{"""This command manages Closure Tables implementation} \CommentTok{It adds new levels and cleans links between entities.} \CommentTok{This way, it\textquotesingle{}s relatively easy to fetch an entire tree with just one tiny request.} \CommentTok{"""} \ImportTok{from}\NormalTok{ django.core.management.base }\ImportTok{import}\NormalTok{ BaseCommand} \ImportTok{from}\NormalTok{ rps.structure.models }\ImportTok{import}\NormalTok{ Entity, EntityTreePath} \KeywordTok{class}\NormalTok{ Command(BaseCommand):} \KeywordTok{def}\NormalTok{ handle(}\VariableTok{self}\NormalTok{, }\OperatorTok{*}\NormalTok{args, }\OperatorTok{**}\NormalTok{options):} \NormalTok{ entities }\OperatorTok{=}\NormalTok{ Entity.objects.}\BuiltInTok{all}\NormalTok{()} \ControlFlowTok{for}\NormalTok{ entity }\KeywordTok{in}\NormalTok{ entities:} \NormalTok{ breadcrumb }\OperatorTok{=}\NormalTok{ [node }\ControlFlowTok{for}\NormalTok{ node }\KeywordTok{in}\NormalTok{ entity.breadcrumb()]} \NormalTok{ tree }\OperatorTok{=} \BuiltInTok{set}\NormalTok{(EntityTreePath.objects.}\BuiltInTok{filter}\NormalTok{(descendant}\OperatorTok{=}\NormalTok{entity))} \ControlFlowTok{for}\NormalTok{ idx, node }\KeywordTok{in} \BuiltInTok{enumerate}\NormalTok{(breadcrumb):} \NormalTok{ tree\_path, \_ }\OperatorTok{=}\NormalTok{ EntityTreePath.objects.get\_or\_create(} \NormalTok{ ancestor}\OperatorTok{=}\NormalTok{node, descendant}\OperatorTok{=}\NormalTok{entity, weight}\OperatorTok{=}\NormalTok{idx }\OperatorTok{+} \DecValTok{1} \NormalTok{ )} \ControlFlowTok{if}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:} \NormalTok{ tree.remove(tree\_path)} \ControlFlowTok{for}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:} \NormalTok{ tree\_path.delete()} \end{Highlighting} \end{Shaded} \hypertarget{_conclusions_3}{% \section{Conclusions}\label{_conclusions_3}} De part son pattern \texttt{MVT}, Django ne fait pas comme les autres frameworks. Pour commencer, nous allons nous concentrer sur la création d'un site ne contenant qu'une seule application, même si en pratique le site contiendra déjà plusieurs applications fournies pas django, comme nous le verrons plus loin. Don't make me think, or why I switched from JS SPAs to Ruby On Rails \url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment} Pour prendre un exemple concret, nous allons créer un site permettant de gérer des listes de souhaits, que nous appellerons \texttt{gwift} (pour \texttt{GiFTs\ and\ WIshlisTs} :)). La première chose à faire est de définir nos besoins du point de vue de l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur puisse faire avec l'application. Ensuite, nous pourrons traduire ces besoins en fonctionnalités et finalement effectuer le développement. \hypertarget{_gwift}{% \section{Gwift}\label{_gwift}} \begin{figure} \centering \includegraphics{images/django/django-project-vs-apps-gwift.png} \caption{Gwift} \end{figure} \hypertarget{_besoins_utilisateurs}{% \section{Besoins utilisateurs}\label{_besoins_utilisateurs}} Nous souhaitons développer un site où un utilisateur donné peut créer une liste contenant des souhaits et où d'autres utilisateurs, authentifiés ou non, peuvent choisir les souhaits à la réalisation desquels ils souhaitent participer. Il sera nécessaire de s'authentifier pour : \begin{itemize} \item Créer une liste associée à l'utilisateur en cours \item Ajouter un nouvel élément à une liste \end{itemize} Il ne sera pas nécessaire de s'authentifier pour : \begin{itemize} \item Faire une promesse d'offre pour un élément appartenant à une liste, associée à un utilisateur. \end{itemize} L'utilisateur ayant créé une liste pourra envoyer un email directement depuis le site aux personnes avec qui il souhaite partager sa liste, cet email contenant un lien permettant d'accéder à cette liste. A chaque souhait, on pourrait de manière facultative ajouter un prix. Dans ce cas, le souhait pourrait aussi être subdivisé en plusieurs parties, de manière à ce que plusieurs personnes puissent participer à sa réalisation. Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à dupliquer le souhait en question. \hypertarget{_besoins_fonctionnels}{% \section{Besoins fonctionnels}\label{_besoins_fonctionnels}} \hypertarget{_gestion_des_utilisateurs}{% \subsection{Gestion des utilisateurs}\label{_gestion_des_utilisateurs}} Pour gérer les utilisateurs, nous allons faire en sorte de surcharger ce que Django propose: par défaut, on a une la possibilité de gérer des utilisateurs (identifiés par une adresse email, un nom, un prénom, \ldots\hspace{0pt}) mais sans plus. Ce qu'on peut souhaiter, c'est que l'utilisateur puisse s'authentifier grâce à une plateforme connue (Facebook, Twitter, Google, etc.), et qu'il puisse un minimum gérer son profil. \hypertarget{_gestion_des_listes}{% \subsection{Gestion des listes}\label{_gestion_des_listes}} \hypertarget{_moduxe8lisation}{% \subsubsection{Modèlisation}\label{_moduxe8lisation}} Les données suivantes doivent être associées à une liste: \begin{itemize} \item un identifiant \item un identifiant externe (un GUID, par exemple) \item un nom \item une description \item le propriétaire, associé à l'utilisateur qui l'aura créée \item une date de création \item une date de modification \end{itemize} \hypertarget{_fonctionnalituxe9s}{% \subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s}} \begin{itemize} \item Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer une liste dont il est le propriétaire \item Un utilisateur doit pouvoir associer ou retirer des souhaits à une liste dont il est le propriétaire \item Il faut pouvoir accéder à une liste, avec un utilisateur authentifier ou non, \textbf{via} son identifiant externe \item Il faut pouvoir envoyer un email avec le lien vers la liste, contenant son identifiant externe \item L'utilisateur doit pouvoir voir toutes les listes qui lui appartiennent \end{itemize} \hypertarget{_gestion_des_souhaits}{% \subsection{Gestion des souhaits}\label{_gestion_des_souhaits}} \hypertarget{_moduxe9lisation_2}{% \subsubsection{Modélisation}\label{_moduxe9lisation_2}} Les données suivantes peuvent être associées à un souhait: \begin{itemize} \item un identifiant \item identifiant de la liste \item un nom \item une description \item le propriétaire \item une date de création \item une date de modification \item une image, afin de représenter l'objet ou l'idée \item un nombre (1 par défaut) \item un prix facultatif \item un nombre de part, facultatif également, si un prix est fourni. \end{itemize} \hypertarget{_fonctionnalituxe9s_2}{% \subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_2}} \begin{itemize} \item Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer un souhait dont il est le propriétaire. \item On ne peut créer un souhait sans liste associée \item Il faut pouvoir fractionner un souhait uniquement si un prix est donné. \item Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié ou non. \item Il faut pouvoir réaliser un souhait ou une partie seulement, avec un utilisateur authentifié ou non. \item Un souhait en cours de réalisation et composé de différentes parts ne peut plus être modifié. \item Un souhait en cours de réalisation ou réalisé ne peut plus être supprimé. \item On peut modifier le nombre de fois qu'un souhait doit être réalisé dans la limite des réalisations déjà effectuées. \end{itemize} \hypertarget{_gestion_des_ruxe9alisations_de_souhaits}{% \subsection{Gestion des réalisations de souhaits}\label{_gestion_des_ruxe9alisations_de_souhaits}} \hypertarget{_moduxe9lisation_3}{% \subsubsection{Modélisation}\label{_moduxe9lisation_3}} Les données suivantes peuvent être associées à une réalisation de souhait: \begin{itemize} \item identifiant du souhait \item identifiant de l'utilisateur si connu \item identifiant de la personne si utilisateur non connu \item un commentaire \item une date de réalisation \end{itemize} \hypertarget{_fonctionnalituxe9s_3}{% \subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_3}} \begin{itemize} \item L'utilisateur doit pouvoir voir si un souhait est réalisé, en partie ou non. Il doit également avoir un pourcentage de complétion sur la possibilité de réalisation de son souhait, entre 0\% et 100\%. \item L'utilisateur doit pouvoir voir la ou les personnes ayant réalisé un souhait. \item Il y a autant de réalisation que de parts de souhait réalisées ou de nombre de fois que le souhait est réalisé. \end{itemize} \hypertarget{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}{% \subsection{Gestion des personnes réalisants les souhaits et qui ne sont pas connues}\label{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}} \hypertarget{_moduxe9lisation_4}{% \subsubsection{Modélisation}\label{_moduxe9lisation_4}} Les données suivantes peuvent être associées à une personne réalisant un souhait: \begin{itemize} \item un identifiant \item un nom \item une adresse email facultative \end{itemize} \hypertarget{_fonctionnalituxe9s_4}{% \subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_4}} Modélisation L'ORM de Django permet de travailler uniquement avec une définition de classes, et de faire en sorte que le lien avec la base de données soit géré uniquement de manière indirecte, par Django lui-même. On peut schématiser ce comportement par une classe = une table. Comme on l'a vu dans la description des fonctionnalités, on va \textbf{grosso modo} avoir besoin des éléments suivants: \begin{itemize} \item Des listes de souhaits \item Des éléments qui composent ces listes \item Des parts pouvant composer chacun de ces éléments \item Des utilisateurs pour gérer tout ceci. \end{itemize} Nous proposons dans un premier temps d'éluder la gestion des utilisateurs, et de simplement se concentrer sur les fonctionnalités principales. Cela nous donne ceci: \begin{enumerate} \def\labelenumi{\alph{enumi}.} \item code-block:: python \begin{verbatim} # wish/models.py \end{verbatim} \begin{verbatim} from django.db import models \end{verbatim} \begin{verbatim} class Wishlist(models.Model): pass \end{verbatim} \begin{verbatim} class Item(models.Model): pass \end{verbatim} \begin{verbatim} class Part(models.Model): pass \end{verbatim} \end{enumerate} Les classes sont créées, mais vides. Entrons dans les détails. Listes de souhaits Comme déjà décrit précédemment, les listes de souhaits peuvent s'apparenter simplement à un objet ayant un nom et une description. Pour rappel, voici ce qui avait été défini dans les spécifications: \begin{itemize} \item un identifiant \item un identifiant externe \item un nom \item une description \item une date de création \item une date de modification \end{itemize} Notre classe \texttt{Wishlist} peut être définie de la manière suivante: \begin{enumerate} \def\labelenumi{\alph{enumi}.} \item code-block:: python \begin{verbatim} # wish/models.py \end{verbatim} \begin{verbatim} class Wishlist(models.Model): \end{verbatim} \begin{verbatim} name = models.CharField(max_length=255) description = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) external_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) \end{verbatim} \end{enumerate} Que peut-on constater? \begin{itemize} \item Que s'il n'est pas spécifié, un identifiant \texttt{id} sera automatiquement généré et accessible dans le modèle. Si vous souhaitez malgré tout spécifier que ce soit un champ en particulier qui devienne la clé primaire, il suffit de l'indiquer grâce à l'attribut \texttt{primary\_key=True}. \item Que chaque type de champs (\texttt{DateTimeField}, \texttt{CharField}, \texttt{UUIDField}, etc.) a ses propres paramètres d'initialisation. Il est intéressant de les apprendre ou de se référer à la documentation en cas de doute. \end{itemize} Au niveau de notre modélisation: \begin{itemize} \item La propriété \texttt{created\_at} est gérée automatiquement par Django grâce à l'attribut \texttt{auto\_now\_add}: de cette manière, lors d'un \textbf{ajout}, une valeur par défaut ("\textbf{maintenant}") sera attribuée à cette propriété. \item La propriété \texttt{updated\_at} est également gérée automatique, cette fois grâce à l'attribut \texttt{auto\_now} initialisé à \texttt{True}: lors d'une \textbf{mise à jour}, la propriété se verra automatiquement assigner la valeur du moment présent. Cela ne permet évidemment pas de gérer un historique complet et ne nous dira pas \textbf{quels champs} ont été modifiés, mais cela nous conviendra dans un premier temps. \item La propriété \texttt{external\_id} est de type \texttt{UUIDField}. Lorsqu'une nouvelle instance sera instanciée, cette propriété prendra la valeur générée par la fonction \texttt{uuid.uuid4()}. \textbf{A priori}, chacun des types de champs possède une propriété \texttt{default}, qui permet d'initialiser une valeur sur une nouvelle instance. \end{itemize} Souhaits Nos souhaits ont besoin des propriétés suivantes: \begin{itemize} \item un identifiant \item l'identifiant de la liste auquel le souhait est lié \item un nom \item une description \item le propriétaire \item une date de création \item une date de modification \item une image permettant de le représenter. \item un nombre (1 par défaut) \item un prix facultatif \item un nombre de part facultatif, si un prix est fourni. \end{itemize} Après implémentation, cela ressemble à ceci: \begin{enumerate} \def\labelenumi{\alph{enumi}.} \item code-block:: python \begin{verbatim} # wish/models.py \end{verbatim} \begin{verbatim} class Wish(models.Model): \end{verbatim} \begin{verbatim} wishlist = models.ForeignKey(Wishlist) name = models.CharField(max_length=255) description = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) picture = models.ImageField() numbers_available = models.IntegerField(default=1) number_of_parts = models.IntegerField(null=True) estimated_price = models.DecimalField(max_digits=19, decimal_places=2, null=True) \end{verbatim} \end{enumerate} A nouveau, que peut-on constater ? \begin{itemize} \item Les clés étrangères sont gérées directement dans la déclaration du modèle. Un champ de type `ForeignKey \textless{}\url{https://docs.djangoproject.com/en/1.8/ref/models/fields/\#django.db.models.ForeignKey\%3E\%60_} permet de déclarer une relation 1-N entre deux classes. Dans la même veine, une relation 1-1 sera représentée par un champ de type `OneToOneField \textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/\%3E\%60}\emph{, alors qu'une relation N-N utilisera un `ManyToManyField \textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/\%3E\%60}}. \item L'attribut \texttt{default} permet de spécifier une valeur initiale, utilisée lors de la construction de l'instance. Cet attribut peut également être une fonction. \item Pour rendre un champ optionnel, il suffit de lui ajouter l'attribut \texttt{null=True}. \item Comme cité ci-dessus, chaque champ possède des attributs spécifiques. Le champ \texttt{DecimalField} possède par exemple les attributs \texttt{max\_digits} et \texttt{decimal\_places}, qui nous permettra de représenter une valeur comprise entre 0 et plus d'un milliard (avec deux chiffres décimaux). \item L'ajout d'un champ de type \texttt{ImageField} nécessite l'installation de \texttt{pillow} pour la gestion des images. Nous l'ajoutons donc à nos pré-requis, dans le fichier \texttt{requirements/base.txt}. \end{itemize} Parts Les parts ont besoins des propriétés suivantes: \begin{itemize} \item un identifiant \item identifiant du souhait \item identifiant de l'utilisateur si connu \item identifiant de la personne si utilisateur non connu \item un commentaire \item une date de réalisation \end{itemize} Elles constituent la dernière étape de notre modélisation et représente la réalisation d'un souhait. Il y aura autant de part d'un souhait que le nombre de souhait à réaliser fois le nombre de part. Elles permettent à un utilisateur de participer au souhait émis par un autre utilisateur. Pour les modéliser, une part est liée d'un côté à un souhait, et d'autre part à un utilisateur. Cela nous donne ceci: \begin{enumerate} \def\labelenumi{\alph{enumi}.} \item code-block:: python \begin{verbatim} from django.contrib.auth.models import User \end{verbatim} \begin{verbatim} class WishPart(models.Model): \end{verbatim} \begin{verbatim} wish = models.ForeignKey(Wish) user = models.ForeignKey(User, null=True) unknown_user = models.ForeignKey(UnknownUser, null=True) comment = models.TextField(null=True, blank=True) done_at = models.DateTimeField(auto_now_add=True) \end{verbatim} \end{enumerate} La classe \texttt{User} référencée au début du snippet correspond à l'utilisateur qui sera connecté. Ceci est géré par Django. Lorsqu'une requête est effectuée et est transmise au serveur, cette information sera disponible grâce à l'objet \texttt{request.user}, transmis à chaque fonction ou \textbf{Class-based-view}. C'est un des avantages d'un framework tout intégré: il vient \textbf{batteries-included} et beaucoup de détails ne doivent pas être pris en compte. Pour le moment, nous nous limiterons à ceci. Par la suite, nous verrons comment améliorer la gestion des profils utilisateurs, comment y ajouter des informations et comment gérer les cas particuliers. La classe \texttt{UnknownUser} permet de représenter un utilisateur non enregistré sur le site et est définie au point suivant. Utilisateurs inconnus \begin{enumerate} \def\labelenumi{\alph{enumi}.} \item todo:: je supprimerais pour que tous les utilisateurs soient gérés au même endroit. \end{enumerate} Pour chaque réalisation d'un souhait par quelqu'un, il est nécessaire de sauver les données suivantes, même si l'utilisateur n'est pas enregistré sur le site: \begin{itemize} \item un identifiant \item un nom \item une adresse email. Cette adresse email sera unique dans notre base de données, pour ne pas créer une nouvelle occurence si un même utilisateur participe à la réalisation de plusieurs souhaits. \end{itemize} Ceci nous donne après implémentation: \begin{enumerate} \def\labelenumi{\alph{enumi}.} \item code-block:: python \begin{verbatim} class UnkownUser(models.Model): \end{verbatim} \begin{verbatim} name = models.CharField(max_length=255) email = models.CharField(email = models.CharField(max_length=255, unique=True) \end{verbatim} \end{enumerate} \hypertarget{_tests_unitaires_2}{% \section{Tests unitaires}\label{_tests_unitaires_2}} \hypertarget{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}{% \subsection{Pourquoi s'ennuyer à écrire des tests?}\label{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}} Traduit grossièrement depuis un article sur `https://medium.com \textless{}\url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_}: \begin{verbatim} Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont \end{verbatim} \begin{verbatim} Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi: \end{verbatim} \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Aide au design: écrire des tests avant d'écrire le code vous donnera une meilleure perspective sur le design à appliquer aux API. \item Documentation (pour les développeurs): chaque description d'un test \item Tester votre compréhension en tant que développeur: \item Assurance qualité: des tests, 5. \end{enumerate} \hypertarget{_why_bother_with_test_discipline}{% \subsection{Why Bother with Test Discipline?}\label{_why_bother_with_test_discipline}} Your tests are your first and best line of defense against software defects. Your tests are more important than linting \& static analysis (which can only find a subclass of errors, not problems with your actual program logic). Tests are as important as the implementation itself (all that matters is that the code meets the requirement --- how it's implemented doesn't matter at all unless it's implemented poorly). Unit tests combine many features that make them your secret weapon to application success: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Design aid: Writing tests first gives you a clearer perspective on the ideal API design. \item Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement. \item Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements? \item Quality Assurance: Manual QA is error prone. In my experience, it's impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features. \item Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production. \end{enumerate} Unit tests don't need to be twisted or manipulated to serve all of those broad-ranging goals. Rather, it is in the essential nature of a unit test to satisfy all of those needs. These benefits are all side-effects of a well-written test suite with good coverage. \hypertarget{_what_are_you_testing}{% \subsection{What are you testing?}\label{_what_are_you_testing}} \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item What component aspect are you testing? \item What should the feature do? What specific behavior requirement are you testing? \end{enumerate} \hypertarget{_couverture_de_code_2}{% \subsection{Couverture de code}\label{_couverture_de_code_2}} On a vu au chapitre 1 qu'il était possible d'obtenir une couverture de code, c'est-à-dire un pourcentage. \hypertarget{_comment_tester}{% \subsection{Comment tester ?}\label{_comment_tester}} Il y a deux manières d'écrire les tests: soit avant, soit après l'implémentation. Oui, idéalement, les tests doivent être écrits à l'avance. Entre nous, on ne va pas râler si vous faites l'inverse, l'important étant que vous le fassiez. Une bonne métrique pour vérifier l'avancement des tests est la couverture de code. Pour l'exemple, nous allons écrire la fonction \texttt{percentage\_of\_completion} sur la classe \texttt{Wish}, et nous allons spécifier les résultats attendus avant même d'implémenter son contenu. Prenons le cas où nous écrivons la méthode avant son test: \begin{Shaded} \begin{Highlighting}[] \KeywordTok{class}\NormalTok{ Wish(models.Model):} \NormalTok{ [...]} \AttributeTok{@property} \KeywordTok{def}\NormalTok{ percentage\_of\_completion(}\VariableTok{self}\NormalTok{):} \CommentTok{"""} \CommentTok{ Calcule le pourcentage de complétion pour un élément.} \CommentTok{ """} \NormalTok{ number\_of\_linked\_parts }\OperatorTok{=}\NormalTok{ WishPart.objects.}\BuiltInTok{filter}\NormalTok{(wish}\OperatorTok{=}\VariableTok{self}\NormalTok{).count()} \NormalTok{ total }\OperatorTok{=} \VariableTok{self}\NormalTok{.number\_of\_parts }\OperatorTok{*} \VariableTok{self}\NormalTok{.numbers\_available} \NormalTok{ percentage }\OperatorTok{=}\NormalTok{ (number\_of\_linked\_parts }\OperatorTok{/}\NormalTok{ total)} \ControlFlowTok{return}\NormalTok{ percentage }\OperatorTok{*} \DecValTok{100} \end{Highlighting} \end{Shaded} Lancez maintenant la couverture de code. Vous obtiendrez ceci: \begin{verbatim} $ coverage run --source "." src/manage.py test wish $ coverage report Name Stmts Miss Branch BrPart Cover ------------------------------------------------------------------ src\gwift\__init__.py 0 0 0 0 100% src\gwift\settings\__init__.py 4 0 0 0 100% src\gwift\settings\base.py 14 0 0 0 100% src\gwift\settings\dev.py 8 0 2 0 100% src\manage.py 6 0 2 1 88% src\wish\__init__.py 0 0 0 0 100% src\wish\admin.py 1 0 0 0 100% src\wish\models.py 36 5 0 0 88% ------------------------------------------------------------------ TOTAL 69 5 4 1 93% \end{verbatim} Si vous générez le rapport HTML avec la commande \texttt{coverage\ html} et que vous ouvrez le fichier \texttt{coverage\_html\_report/src\_wish\_models\_py.html}, vous verrez que les méthodes en rouge ne sont pas testées. \textbf{A contrario}, la couverture de code atteignait \textbf{98\%} avant l'ajout de cette nouvelle méthode. Pour cela, on va utiliser un fichier \texttt{tests.py} dans notre application \texttt{wish}. \textbf{A priori}, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application. \begin{Shaded} \begin{Highlighting}[] \ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase} \KeywordTok{class}\NormalTok{ TestWishModel(TestCase):} \KeywordTok{def}\NormalTok{ test\_percentage\_of\_completion(}\VariableTok{self}\NormalTok{):} \CommentTok{"""} \CommentTok{ Vérifie que le pourcentage de complétion d\textquotesingle{}un souhait} \CommentTok{ est correctement calculé.} \CommentTok{ Sur base d\textquotesingle{}un souhait, on crée quatre parts et on vérifie} \CommentTok{ que les valeurs s\textquotesingle{}étalent correctement sur 25\%, 50\%, 75\% et 100\%.} \CommentTok{ """} \NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist(name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake WishList\textquotesingle{}}\NormalTok{,} \NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wishlist\textquotesingle{}}\NormalTok{)} \NormalTok{ wishlist.save()} \NormalTok{ wish }\OperatorTok{=}\NormalTok{ Wish(wishlist}\OperatorTok{=}\NormalTok{wishlist,} \NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake Wish\textquotesingle{}}\NormalTok{,} \NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wish\textquotesingle{}}\NormalTok{,} \NormalTok{ number\_of\_parts}\OperatorTok{=}\DecValTok{4}\NormalTok{)} \NormalTok{ wish.save()} \NormalTok{ part1 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part1\textquotesingle{}}\NormalTok{)} \NormalTok{ part1.save()} \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{25}\NormalTok{, wish.percentage\_of\_completion)} \NormalTok{ part2 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part2\textquotesingle{}}\NormalTok{)} \NormalTok{ part2.save()} \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{50}\NormalTok{, wish.percentage\_of\_completion)} \NormalTok{ part3 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part3\textquotesingle{}}\NormalTok{)} \NormalTok{ part3.save()} \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{75}\NormalTok{, wish.percentage\_of\_completion)} \NormalTok{ part4 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part4\textquotesingle{}}\NormalTok{)} \NormalTok{ part4.save()} \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{100}\NormalTok{, wish.percentage\_of\_completion)} \end{Highlighting} \end{Shaded} L'attribut \texttt{@property} sur la méthode \texttt{percentage\_of\_completion()} va nous permettre d'appeler directement la méthode \texttt{percentage\_of\_completion()} comme s'il s'agissait d'une propriété de la classe, au même titre que les champs \texttt{number\_of\_parts} ou \texttt{numbers\_available}. Attention que ce type de méthode contactera la base de données à chaque fois qu'elle sera appelée. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de comportement a très peu d'impacts, mais ce n'est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de \textbf{cache}, que nous aborderons plus loin. En relançant la couverture de code, on voit à présent que nous arrivons à 99\%: \begin{verbatim} $ coverage run --source='.' src/manage.py test wish; coverage report; coverage html; . ---------------------------------------------------------------------- Ran 1 test in 0.006s OK Creating test database for alias 'default'... Destroying test database for alias 'default'... Name Stmts Miss Branch BrPart Cover ------------------------------------------------------------------ src\gwift\__init__.py 0 0 0 0 100% src\gwift\settings\__init__.py 4 0 0 0 100% src\gwift\settings\base.py 14 0 0 0 100% src\gwift\settings\dev.py 8 0 2 0 100% src\manage.py 6 0 2 1 88% src\wish\__init__.py 0 0 0 0 100% src\wish\admin.py 1 0 0 0 100% src\wish\models.py 34 0 0 0 100% src\wish\tests.py 20 0 0 0 100% ------------------------------------------------------------------ TOTAL 87 0 4 1 99% \end{verbatim} En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code. \hypertarget{_quelques_liens_utiles}{% \subsection{Quelques liens utiles}\label{_quelques_liens_utiles}} \begin{itemize} \item `Django factory boy \textless{}\url{https://github.com/rbarrois/django-factory_boy/tree/v1.0.0\%3E\%60_} \end{itemize} \hypertarget{_refactoring}{% \section{Refactoring}\label{_refactoring}} On constate que plusieurs classes possèdent les mêmes propriétés \texttt{created\_at} et \texttt{updated\_at}, initialisées aux mêmes valeurs. Pour gagner en cohérence, nous allons créer une classe dans laquelle nous définirons ces deux champs, et nous ferons en sorte que les classes \texttt{Wishlist}, \texttt{Item} et \texttt{Part} en héritent. Django gère trois sortes d'héritage: \begin{itemize} \item L'héritage par classe abstraite \item L'héritage classique \item L'héritage par classe proxy. \end{itemize} \hypertarget{_classe_abstraite}{% \subsection{Classe abstraite}\label{_classe_abstraite}} L'héritage par classe abstraite consiste à déterminer une classe mère qui ne sera jamais instanciée. C'est utile pour définir des champs qui se répèteront dans plusieurs autres classes et surtout pour respecter le principe de DRY. Comme la classe mère ne sera jamais instanciée, ces champs seront en fait dupliqués physiquement, et traduits en SQL, dans chacune des classes filles. \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# wish/models.py} \KeywordTok{class}\NormalTok{ AbstractModel(models.Model):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ abstract }\OperatorTok{=} \VariableTok{True} \NormalTok{ created\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now\_add}\OperatorTok{=}\VariableTok{True}\NormalTok{)} \NormalTok{ updated\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now}\OperatorTok{=}\VariableTok{True}\NormalTok{)} \KeywordTok{class}\NormalTok{ Wishlist(AbstractModel):} \ControlFlowTok{pass} \KeywordTok{class}\NormalTok{ Item(AbstractModel):} \ControlFlowTok{pass} \KeywordTok{class}\NormalTok{ Part(AbstractModel):} \ControlFlowTok{pass} \end{Highlighting} \end{Shaded} En traduisant ceci en SQL, on aura en fait trois tables, chacune reprenant les champs \texttt{created\_at} et \texttt{updated\_at}, ainsi que son propre identifiant: \begin{Shaded} \begin{Highlighting}[] \CommentTok{{-}{-}$ python manage.py sql wish} \ControlFlowTok{BEGIN}\NormalTok{;} \KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (} \OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,} \OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,} \OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL} \NormalTok{)} \NormalTok{;} \KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (} \OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,} \OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,} \OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL} \NormalTok{)} \NormalTok{;} \KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (} \OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,} \OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,} \OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL} \NormalTok{)} \NormalTok{;} \KeywordTok{COMMIT}\NormalTok{;} \end{Highlighting} \end{Shaded} \hypertarget{_huxe9ritage_classique}{% \subsection{Héritage classique}\label{_huxe9ritage_classique}} L'héritage classique est généralement déconseillé, car il peut introduire très rapidement un problème de performances: en reprenant l'exemple introduit avec l'héritage par classe abstraite, et en omettant l'attribut \texttt{abstract\ =\ True}, on se retrouvera en fait avec quatre tables SQL: \begin{itemize} \item Une table \texttt{AbstractModel}, qui reprend les deux champs \texttt{created\_at} et \texttt{updated\_at} \item Une table \texttt{Wishlist} \item Une table \texttt{Item} \item Une table \texttt{Part}. \end{itemize} A nouveau, en analysant la sortie SQL de cette modélisation, on obtient ceci: \begin{Shaded} \begin{Highlighting}[] \CommentTok{{-}{-}$ python manage.py sql wish} \ControlFlowTok{BEGIN}\NormalTok{;} \KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (} \OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,} \OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,} \OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL} \NormalTok{)} \NormalTok{;} \KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (} \OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)} \NormalTok{)} \NormalTok{;} \KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (} \OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)} \NormalTok{)} \NormalTok{;} \KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (} \OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)} \NormalTok{)} \NormalTok{;} \KeywordTok{COMMIT}\NormalTok{;} \end{Highlighting} \end{Shaded} Le problème est que les identifiants seront définis et incrémentés au niveau de la table mère. Pour obtenir les informations héritées, nous seront obligés de faire une jointure. En gros, impossible d'obtenir les données complètes pour l'une des classes de notre travail de base sans effectuer un \textbf{join} sur la classe mère. Dans ce sens, cela va encore\ldots\hspace{0pt} Mais imaginez que vous définissiez une classe \texttt{Wishlist}, de laquelle héritent les classes \texttt{ChristmasWishlist} et \texttt{EasterWishlist}: pour obtenir la liste complètes des listes de souhaits, il vous faudra faire une jointure \textbf{externe} sur chacune des tables possibles, avant même d'avoir commencé à remplir vos données. Il est parfois nécessaire de passer par cette modélisation, mais en étant conscient des risques inhérents. \hypertarget{_classe_proxy}{% \subsection{Classe proxy}\label{_classe_proxy}} Lorsqu'on définit une classe de type \textbf{proxy}, on fait en sorte que cette nouvelle classe ne définisse aucun nouveau champ sur la classe mère. Cela ne change dès lors rien à la traduction du modèle de données en SQL, puisque la classe mère sera traduite par une table, et la classe fille ira récupérer les mêmes informations dans la même table: elle ne fera qu'ajouter ou modifier un comportement dynamiquement, sans ajouter d'emplacements de stockage supplémentaires. Nous pourrions ainsi définir les classes suivantes: \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# wish/models.py} \KeywordTok{class}\NormalTok{ Wishlist(models.Model):} \NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} \NormalTok{ description }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{2000}\NormalTok{)} \NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ models.DateField()} \AttributeTok{@staticmethod} \KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description, expiration\_date}\OperatorTok{=}\VariableTok{None}\NormalTok{):} \NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist()} \NormalTok{ wishlist.name }\OperatorTok{=}\NormalTok{ name} \NormalTok{ wishlist.description }\OperatorTok{=}\NormalTok{ description} \NormalTok{ wishlist.expiration\_date }\OperatorTok{=}\NormalTok{ expiration\_date} \NormalTok{ wishlist.save()} \ControlFlowTok{return}\NormalTok{ wishlist} \KeywordTok{class}\NormalTok{ ChristmasWishlist(Wishlist):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ proxy }\OperatorTok{=} \VariableTok{True} \AttributeTok{@staticmethod} \KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):} \NormalTok{ christmas }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{12}\NormalTok{, }\DecValTok{31}\NormalTok{)} \NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, christmas)} \NormalTok{ w.save()} \KeywordTok{class}\NormalTok{ EasterWishlist(Wishlist):} \KeywordTok{class}\NormalTok{ Meta:} \NormalTok{ proxy }\OperatorTok{=} \VariableTok{True} \AttributeTok{@staticmethod} \KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):} \NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{4}\NormalTok{, }\DecValTok{1}\NormalTok{)} \NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, expiration\_date)} \NormalTok{ w.save()} \end{Highlighting} \end{Shaded} Gestion des utilisateurs Dans les spécifications, nous souhaitions pouvoir associer un utilisateur à une liste (\textbf{le propriétaire}) et un utilisateur à une part (\textbf{le donateur}). Par défaut, Django offre une gestion simplifiée des utilisateurs (pas de connexion LDAP, pas de double authentification, \ldots\hspace{0pt}): juste un utilisateur et un mot de passe. Pour y accéder, un paramètre par défaut est défini dans votre fichier de settings: \texttt{AUTH\_USER\_MODEL}. \hypertarget{_khana}{% \section{Khana}\label{_khana}} Khana est une application de suivi d'apprentissage pour des élèves ou étudiants. Nous voulons pouvoir: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Lister les élèves \item Faire des listes de présence pour les élèves \item Pouvoir planifier ses cours \item Pouvoir suivre l'apprentissage des élèves, les liens qu'ils ont entre les éléments à apprendre: \item pour écrire une phrase, il faut pouvoir écrire des mots, connaître la grammaire, et connaître la conjugaison \item pour écrire des mots, il faut savoir écrire des lettres \item \ldots\hspace{0pt} \end{enumerate} Plusieurs professeurs s'occupent d'une même classe; il faut pouvoir écrire des notes, envoyer des messages aux autres professeurs, etc. Il faut également pouvoir définir des dates de contrôle, voir combien de semaines il reste pour s'assurer d'avoir vu toute la matiètre. Et pouvoir encoder les points des contrôles. \begin{figure} \centering \includegraphics{images/django/django-project-vs-apps-khana.png} \caption{Khana} \end{figure} \end{document}