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