7576 lines
315 KiB
TeX
7576 lines
315 KiB
TeX
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{def}\NormalTok{ test\_add():}
|
||
\ControlFlowTok{assert} \DecValTok{1} \OperatorTok{+} \DecValTok{1} \OperatorTok{==} \StringTok{"argh"}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{λ }\ExtensionTok{pytest}
|
||
\NormalTok{============================= }\BuiltInTok{test}\NormalTok{ session starts ====================================}
|
||
\ExtensionTok{platform}\NormalTok{ ...}
|
||
\ExtensionTok{rootdir}\NormalTok{: ...}
|
||
\ExtensionTok{plugins}\NormalTok{: django{-}4.1.0}
|
||
\ExtensionTok{collected}\NormalTok{ 1 item}
|
||
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}test\_models.py F [100\%]}
|
||
|
||
\NormalTok{================================== }\ExtensionTok{FAILURES}\NormalTok{ ==========================================}
|
||
\ExtensionTok{\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_}\NormalTok{ test\_basic\_add \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_}
|
||
|
||
\ExtensionTok{def}\NormalTok{ test\_basic\_add()}\BuiltInTok{:}
|
||
\OperatorTok{\textgreater{}} \ExtensionTok{assert}\NormalTok{ 1 + 1 == }\StringTok{"argh"}
|
||
\ExtensionTok{E}\NormalTok{ AssertionError: assert (1 + 1) == }\StringTok{\textquotesingle{}argh\textquotesingle{}}
|
||
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}test\_models.py:2: AssertionError}
|
||
|
||
\NormalTok{=========================== }\ExtensionTok{short}\NormalTok{ test summary info ==================================}
|
||
\ExtensionTok{FAILED}\NormalTok{ gwift/test\_models.py::test\_basic\_add {-} AssertionError: assert (1 + 1) == }\StringTok{\textquotesingle{}argh\textquotesingle{}}
|
||
\NormalTok{============================== }\ExtensionTok{1}\NormalTok{ failed in 0.10s =====================================}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_couverture_de_code}{%
|
||
\subsubsection{Couverture de code}\label{_couverture_de_code}}
|
||
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# requirements/base.text}
|
||
\NormalTok{[}\ExtensionTok{...}\NormalTok{]}
|
||
\ExtensionTok{django\_coverage\_plugin}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# .coveragerc to control coverage.py}
|
||
\NormalTok{[}\ExtensionTok{run}\NormalTok{]}
|
||
\ExtensionTok{branch}\NormalTok{ = True}
|
||
\ExtensionTok{omit}\NormalTok{ = ../*migrations*}
|
||
\ExtensionTok{plugins}\NormalTok{ =}
|
||
\ExtensionTok{django\_coverage\_plugin}
|
||
|
||
\NormalTok{[}\ExtensionTok{report}\NormalTok{]}
|
||
\ExtensionTok{ignore\_errors}\NormalTok{ = True}
|
||
|
||
\NormalTok{[}\ExtensionTok{html}\NormalTok{]}
|
||
\ExtensionTok{directory}\NormalTok{ = coverage\_html\_report}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{coverage}\NormalTok{ run {-}{-}source }\StringTok{"."}\NormalTok{ manage.py test}
|
||
\NormalTok{$ }\ExtensionTok{coverage}\NormalTok{ report}
|
||
|
||
\ExtensionTok{Name}\NormalTok{ Stmts Miss Cover}
|
||
\ExtensionTok{{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}\_\_init\_\_.py 0 0 100\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}settings.py 17 0 100\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}urls.py 5 5 0\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}gwift\textbackslash{}wsgi.py 4 4 0\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}manage.py 6 0 100\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}\_\_init\_\_.py 0 0 100\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}admin.py 1 0 100\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}models.py 49 16 67\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}tests.py 1 1 0\%}
|
||
\ExtensionTok{gwift}\NormalTok{\textbackslash{}wish\textbackslash{}views.py 6 6 0\%}
|
||
\ExtensionTok{{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}{-}}
|
||
\ExtensionTok{TOTAL}\NormalTok{ 89 32 64\%}
|
||
\ExtensionTok{{-}{-}{-}{-}}
|
||
|
||
\NormalTok{$ }\ExtensionTok{coverage}\NormalTok{ html}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_dockerfile}{%
|
||
\subsubsection{Dockerfile}\label{_dockerfile}}
|
||
|
||
\begin{verbatim}
|
||
\end{verbatim}
|
||
|
||
\hypertarget{_makefile}{%
|
||
\subsubsection{Makefile}\label{_makefile}}
|
||
|
||
|
||
|
||
|
||
|
||
\hypertarget{_un_terminal}{%
|
||
\subsection{Un terminal}\label{_un_terminal}}
|
||
|
||
\emph{A priori}, les IDE \footnote{Integrated Development Environment}
|
||
proposés ci-dessus fournissent par défaut ou \emph{via} des greffons un
|
||
terminal intégré. Ceci dit, disposer d'un terminal séparé facilite
|
||
parfois certaines tâches.
|
||
|
||
A nouveau, si vous manquez d'idées:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Si vous êtes sous Windows, téléchargez une copie de
|
||
\href{https://cmder.net/}{Cmder}. Il n'est pas le plus rapide, mais
|
||
propose une intégration des outils Unix communs (\texttt{ls},
|
||
\texttt{pwd}, \texttt{grep}, \texttt{ssh}, \texttt{git},
|
||
\ldots\hspace{0pt}) sans trop se fouler.
|
||
\item
|
||
Pour tout autre système, vous devriez disposer en natif de ce qu'il
|
||
faut.
|
||
\end{enumerate}
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/environment/terminal.png}
|
||
\caption{Mise en abîme}
|
||
\end{figure}
|
||
|
||
\hypertarget{_un_gestionnaire_de_base_de_donnuxe9es}{%
|
||
\subsection{Un gestionnaire de base de
|
||
données}\label{_un_gestionnaire_de_base_de_donnuxe9es}}
|
||
|
||
Django gère plusieurs moteurs de base de données. Certains sont gérés
|
||
nativement par Django (PostgreSQL, MariaDB, SQLite); \emph{a priori},
|
||
ces trois-là sont disponibles pour tous les systèmes d'exploitation.
|
||
D'autres moteurs nécessitent des librairies tierces (Oracle, Microsoft
|
||
SQL Server).
|
||
|
||
Il n'est pas obligatoire de disposer d'une application de gestion pour
|
||
ces moteurs: pour les cas d'utilisation simples, le shell Django pourra
|
||
largement suffire (nous y reviendrons). Mais pour faciliter la gestion
|
||
des bases de données elles-même, et si vous n'êtes pas à l'aise avec la
|
||
ligne de commande, choisissez l'une des applications d'administration
|
||
ci-dessous en fonction du moteur de base de données que vous souhaitez
|
||
utiliser.
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Pour \textbf{PostgreSQL}, il existe
|
||
\href{https://www.pgadmin.org/}{pgAdmin}
|
||
\item
|
||
Pour \textbf{MariaDB} ou \textbf{MySQL}, partez sur
|
||
\href{https://www.phpmyadmin.net/}{PHPMyAdmin}
|
||
\item
|
||
Pour \textbf{SQLite}, il existe
|
||
\href{https://sqlitebrowser.org/}{SQLiteBrowser} PHPMyAdmin ou
|
||
PgAdmin.
|
||
\end{itemize}
|
||
|
||
\hypertarget{_un_gestionnaire_de_mots_de_passe}{%
|
||
\subsection{Un gestionnaire de mots de
|
||
passe}\label{_un_gestionnaire_de_mots_de_passe}}
|
||
|
||
Nous en auront besoin pour gé(né)rer des phrases secrètes pour nos
|
||
applications. Si vous n'en utilisez pas déjà un, partez sur
|
||
\href{https://keepassxc.org/}{KeepassXC}: il est multi-plateformes,
|
||
suivi et s'intègre correctement aux différents environnements, tout en
|
||
restant accessible.
|
||
|
||
\includegraphics{images/environment/keepass.png}
|
||
|
||
\hypertarget{_un_systuxe8me_de_gestion_de_versions}{%
|
||
\subsection{Un système de gestion de
|
||
versions}\label{_un_systuxe8me_de_gestion_de_versions}}
|
||
|
||
Il existe plusieurs systèmes de gestion de versions. Le plus connu à
|
||
l'heure actuelle est \href{https://git-scm.com/}{Git}, notamment pour sa
|
||
(très) grande flexibilité et sa rapidité d'exécution. Il est une aide
|
||
précieuse pour développer rapidement des preuves de concept, switcher
|
||
vers une nouvelle fonctionnalité, un bogue à réparer ou une nouvelle
|
||
release à proposer au téléchargement. Ses deux plus gros défauts
|
||
concerneraient peut-être sa courbe d'apprentissage pour les nouveaux
|
||
venus et la complexité des actions qu'il permet de réaliser.
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/xkcd-1597-git.png}
|
||
\caption{\url{https://xkcd.com/1597/}}
|
||
\end{figure}
|
||
|
||
Même pour un développeur solitaire, un système de gestion de versions
|
||
(quel qu'il soit) reste indispensable.
|
||
|
||
Chaque "\textbf{branche}" correspond à une tâche à réaliser: un bogue à
|
||
corriger (\emph{Hotfix A}), une nouvelle fonctionnalité à ajouter ou un
|
||
"\emph{truc à essayer}" \footnote{Oui, comme dans "Attends, j'essaie
|
||
vite un truc, si ça marche, c'est beau."} (\emph{Feature A} et
|
||
\emph{Feature B}).
|
||
|
||
Chaque "\textbf{commit}" correspond à une sauvegarde atomique d'un état
|
||
ou d'un ensemble de modifications cohérentes entre elles.\footnote{Il
|
||
convient donc de s'abstenir de modifier le CSS d'une application et la
|
||
couche d'accès à la base de données, sous peine de se faire huer par
|
||
ses relecteurs au prochain stand-up.} De cette manière, il est
|
||
beaucoup plus facile pour le développeur de se concenter sur un sujet en
|
||
particulier, dans la mesure où celui-ci ne doit pas obligatoirement être
|
||
clôturé pour appliquer un changement de contexte.
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/diagrams/git-workflow.png}
|
||
\caption{Git en action}
|
||
\end{figure}
|
||
|
||
Cas pratique: vous développez cette nouvelle fonctionnalité qui va
|
||
révolutionner le monde de demain et d'après-demain, quand, tout à coup
|
||
(!), vous vous rendez compte que vous avez perdu votre conformité aux
|
||
normes PCI parce les données des titulaires de cartes ne sont pas
|
||
isolées correctement. Il suffit alors de:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
sauver le travail en cours
|
||
(\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ {[}WIP{]}})
|
||
\item
|
||
revenir sur la branche principale (\texttt{git\ checkout\ main})
|
||
\item
|
||
créer un "hotfix" (\texttt{git\ checkout\ -b\ hotfix/pci-compliance})
|
||
\item
|
||
solutionner le problème (sans doute un \texttt{;} en trop ?)
|
||
\item
|
||
sauver le correctif sur cette branche
|
||
(\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ "Did\ it!"})
|
||
\item
|
||
récupérer ce correctif sur la branche principal
|
||
(\texttt{git\ checkout\ main\ \&\&\ git\ merge\ hotfix/pci-compliance})
|
||
\item
|
||
et revenir tranquillou sur votre branche de développement pour
|
||
fignoler ce générateur de noms de dinosaures rigolos que l'univers
|
||
vous réclame à cor et à a cri
|
||
(\texttt{git\ checkout\ features/dinolol})
|
||
\end{enumerate}
|
||
|
||
Finalement, sachez qu'il existe plusieurs manières de gérer ces flux
|
||
d'informations. Les plus connus sont
|
||
\href{https://www.gitflow.com/}{Gitflow} et
|
||
\href{https://www.reddit.com/r/programming/comments/7mfxo6/a_branching_strategy_simpler_than_gitflow/}{Threeflow}.
|
||
|
||
\hypertarget{_duxe9crire_ses_changements}{%
|
||
\subsubsection{Décrire ses
|
||
changements}\label{_duxe9crire_ses_changements}}
|
||
|
||
La description d'un changement se fait \emph{via} la commande
|
||
\texttt{git\ commit}. Il est possible de lui passer directement le
|
||
message associé à ce changement grâce à l'attribut \texttt{-m}, mais
|
||
c'est une pratique relativement déconseillée: un \emph{commit} ne doit
|
||
effectivement pas obligatoirement être décrit sur une seule ligne. Une
|
||
description plus complète, accompagnée des éventuels tickets ou
|
||
références, sera plus complète, plus agréable à lire, et plus facile à
|
||
revoir pour vos éventuels relecteurs.
|
||
|
||
De plus, la plupart des plateformes de dépôts présenteront ces
|
||
informations de manière ergonomique. Par exemple:
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/environment/gitea-commit-message.png}
|
||
\caption{Un exemple de commit affiché dans Gitea}
|
||
\end{figure}
|
||
|
||
La première ligne est reprise comme titre (normalement, sur 50
|
||
caractères maximum); le reste est repris comme de la description.
|
||
|
||
\hypertarget{_un_systuxe8me_de_virtualisation}{%
|
||
\subsection{Un système de
|
||
virtualisation}\label{_un_systuxe8me_de_virtualisation}}
|
||
|
||
Par "\emph{système de virtualisation}", nous entendons n'importe quel
|
||
application, système d'exploitation, système de containeurisation,
|
||
\ldots\hspace{0pt} qui permette de créer ou recréer un environnement de
|
||
développement aussi proche que celui en production. Les solutions sont
|
||
nombreuses:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\href{https://www.virtualbox.org/}{VirtualBox}
|
||
\item
|
||
\href{https://www.vagrantup.com/}{Vagrant}
|
||
\item
|
||
\href{https://www.docker.com/}{Docker}
|
||
\item
|
||
\href{https://linuxcontainers.org/lxc/}{Linux Containers (LXC)}
|
||
\item
|
||
\href{https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v}{Hyper-V}
|
||
\end{itemize}
|
||
|
||
Ces quelques propositions se situent un cran plus loin que la "simple"
|
||
isolation d'un environnement, puisqu'elles vous permettront de
|
||
construire un environnement complet. Elles constituent donc une étape
|
||
supplémentaires dans la configuration de votre espace de travail, mais
|
||
en amélioreront la qualité.
|
||
|
||
Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux
|
||
solutions automatisables et multiplateformes, dont la configuration peut
|
||
faire partie intégrante de vos sources.
|
||
|
||
\hypertarget{_vagrant}{%
|
||
\subsubsection{Vagrant}\label{_vagrant}}
|
||
|
||
Vagrant consiste en un outil de création et de gestion d'environnements
|
||
virtualisés, en respectant toujours une même manière de travailler,
|
||
indépendamment des choix techniques et de l'infrastructure que vous
|
||
pourriez sélectionner.
|
||
|
||
\begin{quote}
|
||
Vagrant is a tool for building and managing virtual machine environments
|
||
in a single workflow. With an easy-to-use workflow and focus on
|
||
automation, Vagrant lowers development environment setup time, increases
|
||
production parity, and makes the "works on my machine" excuse a relic of
|
||
the past. \footnote{\url{https://www.vagrantup.com/intro}}
|
||
\end{quote}
|
||
|
||
La partie la plus importante de la configuration de Vagrant pour votre
|
||
projet consiste à placer un fichier \texttt{Vagrantfile} - \emph{a
|
||
priori} à la racine de votre projet - et qui contiendra les information
|
||
suivantes:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Le choix du \emph{fournisseur} (\textbf{provider}) de virtualisation
|
||
(Virtualbox, Hyper-V et Docker sont natifs; il est également possible
|
||
de passer par VMWare, AWS, etc.)
|
||
\item
|
||
Une \emph{box}, qui consiste à lui indiquer le type et la version
|
||
attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et
|
||
\href{https://app.vagrantup.com/boxes/search}{il y a du choix}).
|
||
\item
|
||
La manière dont la fourniture (\textbf{provisioning}) de
|
||
l'environnement doit être réalisée: scripts Shell, fichiers, Ansible,
|
||
Puppet, Chef, \ldots\hspace{0pt} Choisissez votre favori :-) même s'il
|
||
est toujours possible de passer par une installation et une
|
||
maintenance manuelle, après s'être connecté sur la machine.
|
||
\item
|
||
Si un espace de stockage doit être partagé entre la machine virtuelle
|
||
et l'hôte
|
||
\item
|
||
Les ports qui doivent être transmis de la machine virtuelle vers
|
||
l'hôte.
|
||
\end{itemize}
|
||
|
||
La syntaxe de ce fichier \texttt{Vagrantfile} est en
|
||
\href{https://www.ruby-lang.org/en/}{Ruby}. Vous trouverez ci-dessous un
|
||
exemple, généré (et nettoyé) après avoir exécuté la commande
|
||
\texttt{vagrant\ init}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# {-}*{-} mode: ruby {-}*{-}}
|
||
\CommentTok{\# vi: set ft=ruby :}
|
||
\DataTypeTok{Vagrant}\NormalTok{.configure(}\StringTok{"2"}\NormalTok{) }\KeywordTok{do}\NormalTok{ |config|}
|
||
|
||
\NormalTok{ config.vm.box = }\StringTok{"ubuntu/bionic64"}
|
||
|
||
\NormalTok{ config.vm.network }\StringTok{"forwarded\_port"}\NormalTok{, }\StringTok{guest: }\DecValTok{80}\NormalTok{, }\StringTok{host: }\DecValTok{8080}\NormalTok{, }\StringTok{host\_ip: "127.0.0.1"}
|
||
|
||
\NormalTok{ config.vm.provider }\StringTok{"virtualbox"} \KeywordTok{do}\NormalTok{ |vb|}
|
||
\NormalTok{ vb.gui = }\DecValTok{true}
|
||
\NormalTok{ vb.memory = }\StringTok{"1024"}
|
||
\KeywordTok{end}
|
||
|
||
\NormalTok{ config.vm.provision }\StringTok{"shell"}\NormalTok{, }\StringTok{inline: }\NormalTok{\textless{}\textless{}{-}}\KeywordTok{SHELL}
|
||
\OtherTok{ apt{-}get update}
|
||
\OtherTok{ apt{-}get install {-}y nginx}
|
||
\OtherTok{ }\KeywordTok{SHELL}
|
||
\KeywordTok{end}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Dans le fichier ci-dessus, nous créons:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Une nouvelle machine virtuelle (ie. \emph{invitée}) sous Ubuntu Bionic
|
||
Beaver, en x64
|
||
\item
|
||
Avec une correspondance du port \texttt{80} de la machine vers le port
|
||
\texttt{8080} de l'hôte, en limitant l'accès à celui-ci - accédez à
|
||
\texttt{localhost:8080} et vous accéderez au port \texttt{80} de la
|
||
machine virtuelle.
|
||
\item
|
||
En utilisant Virtualbox comme backend - la mémoire vive allouée sera
|
||
limitée à 1Go de RAM et nous ne voulons pas voir l'interface graphique
|
||
au démarrage
|
||
\item
|
||
Et pour finir, nous voulons appliquer un script de mise à jour
|
||
\texttt{apt-get\ update} et installer le paquet \texttt{nginx}
|
||
\end{itemize}
|
||
|
||
Par défaut, le répertoire courant (ie. le répertoire dans lequel notre
|
||
fichier \texttt{Vagrantfile} se trouve) sera synchronisé dans le
|
||
répertoire \texttt{/vagrant} sur la machine invitée.
|
||
|
||
\hypertarget{_docker}{%
|
||
\subsubsection{Docker}\label{_docker}}
|
||
|
||
(copié/collé de cookie-cutter-django)
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{version: }\StringTok{\textquotesingle{}3\textquotesingle{}}
|
||
|
||
\NormalTok{volumes:}
|
||
\NormalTok{ local\_postgres\_data: \{\}}
|
||
\NormalTok{ local\_postgres\_data\_backups: \{\}}
|
||
|
||
\NormalTok{services:}
|
||
\NormalTok{ django: \&django}
|
||
\NormalTok{ build:}
|
||
\NormalTok{ context: .}
|
||
\NormalTok{ dockerfile: ./compose/local/django/Dockerfile}
|
||
\NormalTok{ image: khana\_local\_django}
|
||
\NormalTok{ container\_name: django}
|
||
\NormalTok{ depends\_on:}
|
||
\NormalTok{ {-} postgres}
|
||
\NormalTok{ volumes:}
|
||
\NormalTok{ {-} .:/app:z}
|
||
\NormalTok{ env\_file:}
|
||
\NormalTok{ {-} ./.envs/.local/.django}
|
||
\NormalTok{ {-} ./.envs/.local/.postgres}
|
||
\NormalTok{ ports:}
|
||
\NormalTok{ {-} }\StringTok{"8000:8000"}
|
||
\NormalTok{ command: /start}
|
||
|
||
\NormalTok{ postgres:}
|
||
\NormalTok{ build:}
|
||
\NormalTok{ context: .}
|
||
\NormalTok{ dockerfile: ./compose/production/postgres/Dockerfile}
|
||
\NormalTok{ image: khana\_production\_postgres}
|
||
\NormalTok{ container\_name: postgres}
|
||
\NormalTok{ volumes:}
|
||
\NormalTok{ {-} local\_postgres\_data:/var/lib/postgresql/data:Z}
|
||
\NormalTok{ {-} local\_postgres\_data\_backups:/backups:z}
|
||
\NormalTok{ env\_file:}
|
||
\NormalTok{ {-} ./.envs/.local/.postgres}
|
||
|
||
\NormalTok{ docs:}
|
||
\NormalTok{ image: khana\_local\_docs}
|
||
\NormalTok{ container\_name: docs}
|
||
\NormalTok{ build:}
|
||
\NormalTok{ context: .}
|
||
\NormalTok{ dockerfile: ./compose/local/docs/Dockerfile}
|
||
\NormalTok{ env\_file:}
|
||
\NormalTok{ {-} ./.envs/.local/.django}
|
||
\NormalTok{ volumes:}
|
||
\NormalTok{ {-} ./docs:/docs:z}
|
||
\NormalTok{ {-} ./config:/app/config:z}
|
||
\NormalTok{ {-} ./khana:/app/khana:z}
|
||
\NormalTok{ ports:}
|
||
\NormalTok{ {-} }\StringTok{"7000:7000"}
|
||
\NormalTok{ command: /start{-}docs}
|
||
|
||
\NormalTok{ redis:}
|
||
\NormalTok{ image: redis:5.0}
|
||
\NormalTok{ container\_name: redis}
|
||
|
||
\NormalTok{ celeryworker:}
|
||
\NormalTok{ \textless{}\textless{}: *django}
|
||
\NormalTok{ image: khana\_local\_celeryworker}
|
||
\NormalTok{ container\_name: celeryworker}
|
||
\NormalTok{ depends\_on:}
|
||
\NormalTok{ {-} redis}
|
||
\NormalTok{ {-} postgres}
|
||
|
||
\NormalTok{ ports: []}
|
||
\NormalTok{ command: /start{-}celeryworker}
|
||
|
||
\NormalTok{ celerybeat:}
|
||
\NormalTok{ \textless{}\textless{}: *django}
|
||
\NormalTok{ image: khana\_local\_celerybeat}
|
||
\NormalTok{ container\_name: celerybeat}
|
||
\NormalTok{ depends\_on:}
|
||
\NormalTok{ {-} redis}
|
||
\NormalTok{ {-} postgres}
|
||
|
||
\NormalTok{ ports: []}
|
||
\NormalTok{ command: /start{-}celerybeat}
|
||
|
||
\NormalTok{ flower:}
|
||
\NormalTok{ \textless{}\textless{}: *django}
|
||
\NormalTok{ image: khana\_local\_flower}
|
||
\NormalTok{ container\_name: flower}
|
||
\NormalTok{ ports:}
|
||
\NormalTok{ {-} }\StringTok{"5555:5555"}
|
||
\NormalTok{ command: /start{-}flower}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# docker{-}compose.yml}
|
||
\NormalTok{version: }\StringTok{\textquotesingle{}3.8\textquotesingle{}}
|
||
|
||
\NormalTok{services:}
|
||
\NormalTok{ web:}
|
||
\NormalTok{ build: .}
|
||
\NormalTok{ command: python /code/manage.py runserver 0.0.0.0:8000}
|
||
\NormalTok{ volumes:}
|
||
\NormalTok{ {-} .:/code}
|
||
\NormalTok{ ports:}
|
||
\NormalTok{ {-} 8000:8000}
|
||
\NormalTok{ depends\_on:}
|
||
\NormalTok{ {-} slqserver}
|
||
\NormalTok{ slqserver:}
|
||
\NormalTok{ image: mcr.microsoft.com/mssql/server:2019{-}latest}
|
||
\NormalTok{ environment:}
|
||
\NormalTok{ {-} }\StringTok{"ACCEPT\_EULA=Y"}
|
||
\NormalTok{ {-} }\StringTok{"SA\_PASSWORD=sqklgjqihagrtdgqk12§!"}
|
||
\NormalTok{ ports:}
|
||
\NormalTok{ {-} 1433:1433}
|
||
\NormalTok{ volumes:}
|
||
\NormalTok{ {-} ../sqlserver/data:/var/opt/mssql/data}
|
||
\NormalTok{ {-} ../sqlserver/log:/var/opt/mssql/log}
|
||
\NormalTok{ {-} ../sqlserver/secrets:/var/opt/mssql/secrets}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{FROM}\NormalTok{ python:3.8{-}slim{-}buster}
|
||
|
||
\KeywordTok{ENV}\NormalTok{ PYTHONUNBUFFERED 1}
|
||
\KeywordTok{ENV}\NormalTok{ PYTHONDONTWRITEBYTECODE 1}
|
||
|
||
\KeywordTok{RUN}\NormalTok{ apt{-}get update \textbackslash{}}
|
||
\CommentTok{\# dependencies for building Python packages}
|
||
\NormalTok{ \&\& apt{-}get install {-}y build{-}essential \textbackslash{}}
|
||
\CommentTok{\# psycopg2 dependencies}
|
||
\NormalTok{ \&\& apt{-}get install {-}y libpq{-}dev \textbackslash{}}
|
||
\CommentTok{\# Translations dependencies}
|
||
\NormalTok{ \&\& apt{-}get install {-}y gettext \textbackslash{}}
|
||
\CommentTok{\# cleaning up unused files}
|
||
\NormalTok{ \&\& apt{-}get purge {-}y {-}{-}auto{-}remove {-}o APT::AutoRemove::RecommendsImportant=false \textbackslash{}}
|
||
\NormalTok{ \&\& rm {-}rf /var/lib/apt/lists/*}
|
||
|
||
\CommentTok{\# Requirements are installed here to ensure they will be cached.}
|
||
\KeywordTok{COPY}\NormalTok{ ./requirements /requirements}
|
||
\KeywordTok{RUN}\NormalTok{ pip install {-}r /requirements/local.txt}
|
||
|
||
\KeywordTok{COPY}\NormalTok{ ./compose/production/django/entrypoint /entrypoint}
|
||
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /entrypoint}
|
||
\KeywordTok{RUN}\NormalTok{ chmod +x /entrypoint}
|
||
|
||
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/start /start}
|
||
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start}
|
||
\KeywordTok{RUN}\NormalTok{ chmod +x /start}
|
||
|
||
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/celery/worker/start /start{-}celeryworker}
|
||
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start{-}celeryworker}
|
||
\KeywordTok{RUN}\NormalTok{ chmod +x /start{-}celeryworker}
|
||
|
||
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/celery/beat/start /start{-}celerybeat}
|
||
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start{-}celerybeat}
|
||
\KeywordTok{RUN}\NormalTok{ chmod +x /start{-}celerybeat}
|
||
|
||
\KeywordTok{COPY}\NormalTok{ ./compose/local/django/celery/flower/start /start{-}flower}
|
||
\KeywordTok{RUN}\NormalTok{ sed {-}i }\StringTok{\textquotesingle{}s/\textbackslash{}r$//g\textquotesingle{}}\NormalTok{ /start{-}flower}
|
||
\KeywordTok{RUN}\NormalTok{ chmod +x /start{-}flower}
|
||
|
||
\KeywordTok{WORKDIR}\NormalTok{ /app}
|
||
|
||
\KeywordTok{ENTRYPOINT}\NormalTok{ [}\StringTok{"/entrypoint"}\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Voir comment nous pouvons intégrer toutes ces commandes au niveau de la
|
||
CI et au niveau du déploiement (Docker-compose ?)
|
||
|
||
\hypertarget{_base_de_donnuxe9es}{%
|
||
\subsubsection{Base de données}\label{_base_de_donnuxe9es}}
|
||
|
||
Parfois, SQLite peut être une bonne option:
|
||
|
||
\begin{quote}
|
||
Write througput is the area where SQLite struggles the most, but there's
|
||
not a ton of compelling data online about how it fares, so I got some of
|
||
my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly
|
||
modified1 version of the SQLite kvtest2 program on it. Writing 512 byte
|
||
blobs as separate transactions, in WAL mode with synchronous=normal3,
|
||
temp\_store=memory, and mmap enabled, I got 13.78μs per write, or
|
||
\textasciitilde72,568 writes per second. Going a bit larger, at 32kb
|
||
writes, I got 303.74μs per write, or \textasciitilde3,292 writes per
|
||
second. That's not astronomical, but it's certainly way more than most
|
||
websites being used by humans need. If you had 10 million daily active
|
||
users, each one could get more than 600 writes per day with that.
|
||
\end{quote}
|
||
|
||
\begin{quote}
|
||
Looking at read throughput, SQLite can go pretty far: with the same test
|
||
above, I got a read throughput of \textasciitilde496,770 reads/sec
|
||
(2.013μs/read) for the 512 byte blob. Other people also report similar
|
||
results --- Expensify reports that you can get 4M QPS if you're willing
|
||
to make some slightly more involved changes and use a beefier server.
|
||
Four million QPS is enough that every internet user in the world could
|
||
make \textasciitilde70 queries per day, with a little headroom left
|
||
over4. Most websites don't need that kind of throughput.
|
||
cite:{[}consider\_sqlite{]}
|
||
\end{quote}
|
||
|
||
\hypertarget{_duxe9marrer_un_nouveau_projet}{%
|
||
\section{Démarrer un nouveau
|
||
projet}\label{_duxe9marrer_un_nouveau_projet}}
|
||
|
||
\hypertarget{_travailler_en_isolation}{%
|
||
\subsection{Travailler en isolation}\label{_travailler_en_isolation}}
|
||
|
||
Nous allons aborder la gestion et l'isolation des dépendances. Cette
|
||
section est aussi utile pour une personne travaillant seule, que pour
|
||
transmettre les connaissances à un nouveau membre de l'équipe ou pour
|
||
déployer l'application elle-même.
|
||
|
||
Il en était déjà question au deuxième point des 12 facteurs: même dans
|
||
le cas de petits projets, il est déconseillé de s'en passer. Cela évite
|
||
les déploiements effectués à l'arrache à grand renfort de \texttt{sudo}
|
||
et d'installation globale de dépendances, pouvant potentiellement
|
||
occasioner des conflits entre les applications déployées:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Il est tout à fait envisagable que deux applications différentes
|
||
soient déployées sur un même hôte, et nécessitent chacune deux
|
||
versions différentes d'une même dépendance.
|
||
\item
|
||
Pour la reproductibilité d'un environnement spécifique, cela évite
|
||
notamment les réponses type "Ca juste marche chez moi", puisque la
|
||
construction d'un nouvel environnement fait partie intégrante du
|
||
processus de construction et de la documentation du projet; grâce à
|
||
elle, nous avons la possibilité de construire un environnement sain et
|
||
d'appliquer des dépendances identiques, quelle que soit la machine
|
||
hôte.
|
||
\end{enumerate}
|
||
|
||
\includegraphics{images/it-works-on-my-machine.jpg}
|
||
|
||
Dans la suite de ce chapitre, nous allons considérer deux projets
|
||
différents:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Gwift, une application permettant de gérer des listes de souhaits
|
||
\item
|
||
Khana, une application de suivi d'apprentissage pour des élèves ou
|
||
étudiants.
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_roulements_de_versions}{%
|
||
\subsubsection{Roulements de versions}\label{_roulements_de_versions}}
|
||
|
||
Django fonctionne sur un
|
||
\href{https://docs.djangoproject.com/en/dev/internals/release-process/}{roulement
|
||
de trois versions mineures pour une version majeure}, clôturé par une
|
||
version LTS (\emph{Long Term Support}).
|
||
|
||
\includegraphics{images/django-support-lts.png}
|
||
|
||
La version utilisée sera une bonne indication à prendre en considération
|
||
pour nos dépendances, puisqu'en visant une version particulière, nous ne
|
||
devrons pratiquement pas nous soucier (bon, un peu quand même, mais nous
|
||
le verrons plus tard\ldots\hspace{0pt}) des dépendances à installer,
|
||
pour peu que l'on reste sous un certain seuil.
|
||
|
||
Dans les étapes ci-dessous, nous épinglerons une version LTS afin de
|
||
nous assurer une certaine sérénité d'esprit (= dont nous ne occuperons
|
||
pas pendant les 3 prochaines années).
|
||
|
||
\hypertarget{_environnements_virtuels}{%
|
||
\subsubsection{Environnements virtuels}\label{_environnements_virtuels}}
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/xkcd-1987.png}
|
||
\caption{\url{https://xkcd.com/1987}}
|
||
\end{figure}
|
||
|
||
Un des reproches que l'on peut faire au langage concerne sa versatilité:
|
||
il est possible de réaliser beaucoup de choses, mais celles-ci ne sont
|
||
pas toujours simples ou directes. Pour quelqu'un qui débarquererait, la
|
||
quantité d'options différentes peut paraître rebutante. Nous pensons
|
||
notamment aux environnements virtuels: ils sont géniaux à utiliser, mais
|
||
on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version
|
||
améliorée et plus ergonomique), \texttt{venv} (la version intégrée
|
||
depuis la version 3.3 de l'interpréteur, et
|
||
\href{https://docs.python.org/3/library/venv.html}{la manière
|
||
recommandée} de créer un environnement depuis la 3.5).
|
||
|
||
Pour créer un nouvel environnement, vous aurez donc besoin:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
D'une installation de Python - \url{https://www.python.org/}
|
||
\item
|
||
D'un terminal - voir le point
|
||
\href{../environment/_index.xml\#un-terminal}{Un terminal}
|
||
\end{enumerate}
|
||
|
||
Il existe plusieurs autres modules permettant d'arriver au même
|
||
résultat, avec quelques avantages et inconvénients pour chacun d'entre
|
||
eux. Le plus prometteur d'entre eux est
|
||
\href{https://python-poetry.org/}{Poetry}, qui dispose d'une interface
|
||
en ligne de commande plus propre et plus moderne que ce que PIP propose.
|
||
|
||
Poetry se propose de gérer le projet au travers d'un fichier
|
||
pyproject.toml. TOML (du nom de son géniteur, Tom Preston-Werner,
|
||
légèrement CEO de GitHub à ses heures), se place comme alternative aux
|
||
formats comme JSON, YAML ou INI.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ExtensionTok{La}\NormalTok{ commande poetry new }\OperatorTok{\textless{}}\NormalTok{project}\OperatorTok{\textgreater{}}\NormalTok{ créera une structure par défaut relativement compréhensible:}
|
||
|
||
\NormalTok{$ }\ExtensionTok{poetry}\NormalTok{ new django{-}gecko}
|
||
\NormalTok{$ }\ExtensionTok{tree}\NormalTok{ django{-}gecko/}
|
||
\ExtensionTok{django{-}gecko/}
|
||
\NormalTok{├── }\ExtensionTok{django\_gecko}
|
||
\NormalTok{│ └── }\ExtensionTok{\_\_init\_\_.py}
|
||
\NormalTok{├── }\ExtensionTok{pyproject.toml}
|
||
\NormalTok{├── }\ExtensionTok{README.rst}
|
||
\NormalTok{└── }\ExtensionTok{tests}
|
||
\NormalTok{ ├── }\ExtensionTok{\_\_init\_\_.py}
|
||
\NormalTok{ └── }\ExtensionTok{test\_django\_gecko.py}
|
||
|
||
\ExtensionTok{2}\NormalTok{ directories, 5 files}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Ceci signifie que nous avons directement (et de manière standard):
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Un répertoire django-gecko, qui porte le nom de l'application que vous
|
||
venez de créer
|
||
\item
|
||
Un répertoires tests, libellé selon les standards de pytest
|
||
\item
|
||
Un fichier README.rst (qui ne contient encore rien)
|
||
\item
|
||
Un fichier pyproject.toml, qui contient ceci:
|
||
\end{itemize}
|
||
|
||
\begin{verbatim}
|
||
[tool.poetry]
|
||
name = "django-gecko"
|
||
version = "0.1.0"
|
||
description = ""
|
||
authors = ["... <...@grimbox.be>"]
|
||
|
||
[tool.poetry.dependencies]
|
||
python = "^3.9"
|
||
|
||
[tool.poetry.dev-dependencies]
|
||
pytest = "^5.2"
|
||
|
||
[build-system]
|
||
requires = ["poetry-core>=1.0.0"]
|
||
build-backend = "poetry.core.masonry.api"
|
||
\end{verbatim}
|
||
|
||
La commande \texttt{poetry\ init} permet de générer interactivement les
|
||
fichiers nécessaires à son intégration dans un projet existant.
|
||
|
||
J'ai pour habitude de conserver mes projets dans un répertoire
|
||
\texttt{\textasciitilde{}/Sources/} et mes environnements virtuels dans
|
||
un répertoire \texttt{\textasciitilde{}/.venvs/}.
|
||
|
||
Cette séparation évite que l'environnement virtuel ne se trouve dans le
|
||
même répertoire que les sources, ou ne soit accidentellement envoyé vers
|
||
le système de gestion de versions. Elle évite également de rendre ce
|
||
répertoire "visible" - il ne s'agit au fond que d'un paramètre de
|
||
configuration lié uniquement à votre environnement de développement; les
|
||
environnements virtuels étant disposables, il n'est pas conseillé de
|
||
trop les lier au projet qui l'utilise comme base. Dans la suite de ce
|
||
chapitre, je considérerai ces mêmes répertoires, mais n'hésitez pas à
|
||
les modifier.
|
||
|
||
DANGER: Indépendamment de l'endroit où vous stockerez le répertoire
|
||
contenant cet environnement, il est primordial de \textbf{ne pas le
|
||
conserver dans votre dépôt de stockager}. Cela irait à l'encontre des
|
||
douze facteurs, cela polluera inutilement vos sources et créera des
|
||
conflits avec l'environnement des personnes qui souhaiteraient
|
||
intervenir sur le projet.
|
||
|
||
Pur créer notre répertoire de travail et notre environnement virtuel,
|
||
exécutez les commandes suivantes:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\FunctionTok{mkdir}\NormalTok{ \textasciitilde{}/.venvs/}
|
||
\ExtensionTok{python}\NormalTok{ {-}m venv \textasciitilde{}/.venvs/gwift{-}venv}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Ceci aura pour effet de créer un nouveau répertoire
|
||
(\texttt{\textasciitilde{}/.venvs/gwift-env/}), dans lequel vous
|
||
trouverez une installation complète de l'interpréteur Python. Votre
|
||
environnement virtuel est prêt, il n'y a plus qu'à indiquer que nous
|
||
souhaitons l'utiliser, grâce à l'une des commandes suivantes:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# GNU/Linux, macOS}
|
||
\BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/gwift{-}venv/bin/activate}
|
||
|
||
\CommentTok{\# MS Windows, avec Cmder}
|
||
\ExtensionTok{\textasciitilde{}/.venvs/gwift{-}venv/Scripts/activate.bat}
|
||
|
||
\CommentTok{\# Pour les deux}
|
||
\KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/.venvs/gwift{-}env$ }
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Le terminal signale que nous sommes bien dans l'environnement
|
||
\texttt{gwift-env}.
|
||
\end{itemize}
|
||
|
||
A présent que l'environnement est activé, tous les binaires de cet
|
||
environnement prendront le pas sur les binaires du système. De la même
|
||
manière, une variable \texttt{PATH} propre est définie et utilisée, afin
|
||
que les librairies Python y soient stockées. C'est donc dans cet
|
||
environnement virtuel que nous retrouverons le code source de Django,
|
||
ainsi que des librairies externes pour Python une fois que nous les
|
||
aurons installées.
|
||
|
||
Pour les curieux, un environnement virtuel n'est jamais qu'un répertoire
|
||
dans lequel se trouve une installation fraîche de l'interpréteur, vers
|
||
laquelle pointe les liens symboliques des binaires. Si vous recherchez
|
||
l'emplacement de l'interpréteur avec la commande \texttt{which\ python},
|
||
vous recevrez comme réponse
|
||
\texttt{/home/fred/.venvs/gwift-env/bin/python}.
|
||
|
||
Pour sortir de l'environnement virtuel, exécutez la commande
|
||
\texttt{deactivate}. Si vous pensez ne plus en avoir besoin, supprimer
|
||
le dossier. Si nécessaire, il suffira d'en créer un nouveau.
|
||
|
||
Pour gérer des versions différentes d'une même librairie, il nous suffit
|
||
de jongler avec autant d'environnements que nécessaires. Une application
|
||
nécessite une version de Django inférieure à la 2.0 ? On crée un
|
||
environnement, on l'active et on installe ce qu'il faut.
|
||
|
||
Cette technique fonctionnera autant pour un poste de développement que
|
||
sur les serveurs destinés à recevoir notre application.
|
||
|
||
Par la suite, nous considérerons que l'environnement virtuel est
|
||
toujours activé, même si \texttt{gwift-env} n'est pas indiqué.
|
||
|
||
a manière recommandée pour la gestion des dépendances consiste à les
|
||
épingler dans un fichier requirements.txt, placé à la racine du projet.
|
||
Ce fichier reprend, ligne par ligne, chaque dépendance et la version
|
||
nécessaire. Cet épinglage est cependant relativement basique, dans la
|
||
mesure où les opérateurs disponibles sont ==, ⇐ et \textgreater=.
|
||
|
||
Poetry propose un épinglage basé sur SemVer. Les contraintes qui peuvent
|
||
être appliquées aux dépendances sont plus touffues que ce que proposent
|
||
pip -r, avec la présence du curseur \^{}, qui ne modifiera pas le nombre
|
||
différent de zéro le plus à gauche:
|
||
|
||
\begin{verbatim}
|
||
^1.2.3 (où le nombre en question est 1) pourra proposer une mise à jour jusqu'à la version juste avant la version 2.0.0
|
||
^0.2.3 pourra être mise à jour jusqu'à la version juste avant 0.3.0.
|
||
...
|
||
\end{verbatim}
|
||
|
||
L'avantage est donc que l'on spécifie une version majeure - mineure -
|
||
patchée, et que l'on pourra spécifier accepter toute mise à jour jusqu'à
|
||
la prochaine version majeure - mineure patchée (non incluse 😉).
|
||
|
||
Une bonne pratique consiste également, tout comme pour npm, à intégrer
|
||
le fichier de lock (poetry.lock) dans le dépôt de sources: de cette
|
||
manière, seules les dépendances testées (et intégrées) seront
|
||
considérées sur tous les environnements de déploiement.
|
||
|
||
Il est alors nécessaire de passer par une action manuelle (poetry
|
||
update) pour mettre à jour le fichier de verrou, et assurer une mise à
|
||
jour en sécurité (seules les dépendances testées sont prises en compte)
|
||
et de qualité (tous les environnements utilisent la même version d'une
|
||
dépendance).
|
||
|
||
L'ajout d'une nouvelle dépendance à un projet se réalise grâce à la
|
||
commande \texttt{poetry\ add\ \textless{}dep\textgreater{}}:
|
||
|
||
\begin{verbatim}
|
||
$ poetry add django
|
||
Using version ^3.2.3 for Django
|
||
|
||
Updating dependencies
|
||
Resolving dependencies... (5.1s)
|
||
|
||
Writing lock file
|
||
|
||
Package operations: 8 installs, 1 update, 0 removals
|
||
|
||
• Installing pyparsing (2.4.7)
|
||
• Installing attrs (21.2.0)
|
||
• Installing more-itertools (8.8.0)
|
||
• Installing packaging (20.9)
|
||
• Installing pluggy (0.13.1)
|
||
• Installing py (1.10.0)
|
||
• Installing wcwidth (0.2.5)
|
||
• Updating django (3.2 -> 3.2.3)
|
||
• Installing pytest (5.4.3)
|
||
\end{verbatim}
|
||
|
||
Elle est ensuite ajoutée à notre fichier \texttt{pyproject.toml}:
|
||
|
||
\begin{verbatim}
|
||
[...]
|
||
|
||
[tool.poetry.dependencies]
|
||
python = "^3.9"
|
||
Django = "^3.2.3"
|
||
|
||
[...]
|
||
\end{verbatim}
|
||
|
||
Et contrairement à \texttt{pip}, pas besoin de savoir s'il faut pointer
|
||
vers un fichier (\texttt{-r}) ou un dépôt VCS (\texttt{-e}), puisque
|
||
Poetry va tout essayer, {[}dans un certain
|
||
ordre{]}(\url{https://python-poetry.org/docs/cli/\#add}). L'avantage
|
||
également (et cela m'arrive encore souvent, ce qui fait hurler le runner
|
||
de Gitlab), c'est qu'il n'est plus nécessaire de penser à épingler la
|
||
dépendance que l'on vient d'installer parmi les fichiers de
|
||
requirements, puisqu'elles s'y ajoutent automatiquement grâce à la
|
||
commande \texttt{add}.
|
||
|
||
\hypertarget{_python_packaging_made_easy}{%
|
||
\subsubsection{Python packaging made
|
||
easy}\label{_python_packaging_made_easy}}
|
||
|
||
Cette partie dépasse mes compétences et connaissances, dans la mesure où
|
||
je n'ai jamais rien packagé ni publié sur {[}pypi.org{]}(pypi.org). Ce
|
||
n'est pas l'envie qui manque, mais les idées et la nécessité 😉. Ceci
|
||
dit, Poetry propose un ensemble de règles et une préconfiguration qui
|
||
(doivent) énormément facilite(r) la mise à disposition de librairies sur
|
||
Pypi - et rien que ça, devrait ouvrir une partie de l'écosystème.
|
||
|
||
Les chapitres 7 et 8 de {[}Expert Python Programming - Third
|
||
Edtion{]}(\#), écrit par Michal Jaworski et Tarek Ziadé en parlent très
|
||
bien:
|
||
|
||
\begin{quote}
|
||
Python packaging can be a bit overwhelming at first. The main reason for
|
||
that is the confusion about proper tools for creating Python packages.
|
||
Anyway, once you create your first package, you will se that this is as
|
||
hard as it looks. Also, knowing propre, state-of-the-art packaging helps
|
||
a lot.
|
||
\end{quote}
|
||
|
||
En gros, c'est ardu-au-début-mais-plus-trop-après. Et c'est heureusement
|
||
suivi et documenté par la PyPA
|
||
(\textbf{\href{https://github.com/pypa}{Python Packaging Authority}}).
|
||
|
||
Les étapes sont les suivantes:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Utiliser setuptools pour définir les projets et créer les
|
||
distributions sources,
|
||
\item
|
||
Utiliser \textbf{wheels} pour créer les paquets,
|
||
\item
|
||
Passer par \textbf{twine} pour envoyer ces paquets vers PyPI
|
||
\item
|
||
Définir un ensemble d'actions (voire, de plugins nécessaires - lien
|
||
avec le VCS, etc.) dans le fichier \texttt{setup.py}, et définir les
|
||
propriétés du projet ou de la librairie dans le fichier
|
||
\texttt{setup.cfg}.
|
||
\end{enumerate}
|
||
|
||
Avec Poetry, deux commandes suffisent (théoriquement - puisque je n'ai
|
||
pas essayé 🤪): \texttt{poetry\ build} et \texttt{poetry\ publish}:
|
||
|
||
\begin{verbatim}
|
||
$ poetry build
|
||
Building geco (0.1.0)
|
||
- Building sdist
|
||
- Built geco-0.1.0.tar.gz
|
||
- Building wheel
|
||
- Built geco-0.1.0-py3-none-any.whl
|
||
|
||
$ tree dist/
|
||
dist/
|
||
├── geco-0.1.0-py3-none-any.whl
|
||
└── geco-0.1.0.tar.gz
|
||
|
||
0 directories, 2 files
|
||
\end{verbatim}
|
||
|
||
Ce qui est quand même 'achement plus simple que d'appréhender tout un
|
||
écosystème.
|
||
|
||
\hypertarget{_gestion_des_duxe9pendances_installation_de_django_et_cruxe9ation_dun_nouveau_projet}{%
|
||
\subsubsection{Gestion des dépendances, installation de Django et
|
||
création d'un nouveau
|
||
projet}\label{_gestion_des_duxe9pendances_installation_de_django_et_cruxe9ation_dun_nouveau_projet}}
|
||
|
||
Comme nous en avons déjà discuté, PIP est la solution que nous avons
|
||
choisie pour la gestion de nos dépendances. Pour installer une nouvelle
|
||
librairie, vous pouvez simplement passer par la commande
|
||
\texttt{pip\ install\ \textless{}my\_awesome\_library\textgreater{}}.
|
||
Dans le cas de Django, et après avoir activé l'environnement, nous
|
||
pouvons à présent y installer Django. Comme expliqué ci-dessus, la
|
||
librairie restera indépendante du reste du système, et ne polluera aucun
|
||
autre projet. nous exécuterons donc la commande suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/gwift{-}env/bin/activate }\CommentTok{\# ou \textasciitilde{}/.venvs/gwift{-}env/Scrips/activate.bat pour Windows.}
|
||
\NormalTok{$ }\ExtensionTok{pip}\NormalTok{ install django}
|
||
\ExtensionTok{Collecting}\NormalTok{ django}
|
||
\ExtensionTok{Downloading}\NormalTok{ Django{-}3.1.4}
|
||
\ExtensionTok{100\%} \KeywordTok{|}\NormalTok{\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#}\KeywordTok{|}
|
||
\ExtensionTok{Installing}\NormalTok{ collected packages: django}
|
||
\ExtensionTok{Successfully}\NormalTok{ installed django{-}3.1.4}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Ici, la commande \texttt{pip\ install\ django} récupère la
|
||
\textbf{dernière version connue disponible dans les dépôts
|
||
\url{https://pypi.org/}} (sauf si vous en avez définis d'autres. Mais
|
||
c'est hors sujet). Nous en avons déjà discuté: il est important de bien
|
||
spécifier la version que vous souhaitez utiliser, sans quoi vous risquez
|
||
de rencontrer des effets de bord.
|
||
|
||
L'installation de Django a ajouté un nouvel exécutable:
|
||
\texttt{django-admin}, que l'on peut utiliser pour créer notre nouvel
|
||
espace de travail. Par la suite, nous utiliserons \texttt{manage.py},
|
||
qui constitue un \textbf{wrapper} autour de \texttt{django-admin}.
|
||
|
||
Pour démarrer notre projet, nous lançons
|
||
\texttt{django-admin\ startproject\ gwift}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{django{-}admin}\NormalTok{ startproject gwift}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Cette action a pour effet de créer un nouveau dossier \texttt{gwift},
|
||
dans lequel nous trouvons la structure suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{tree}\NormalTok{ gwift}
|
||
\ExtensionTok{gwift}
|
||
\NormalTok{├── }\ExtensionTok{gwift}
|
||
\KeywordTok{|} \KeywordTok{|}\NormalTok{── }\ExtensionTok{asgi.py}
|
||
\NormalTok{│ ├── }\ExtensionTok{\_\_init\_\_.py}
|
||
\NormalTok{│ ├── }\ExtensionTok{settings.py}
|
||
\NormalTok{│ ├── }\ExtensionTok{urls.py}
|
||
\NormalTok{│ └── }\ExtensionTok{wsgi.py}
|
||
\NormalTok{└── }\ExtensionTok{manage.py}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
C'est dans ce répertoire que vont vivre tous les fichiers liés au
|
||
projet. Le but est de faire en sorte que toutes les opérations
|
||
(maintenance, déploiement, écriture, tests, \ldots\hspace{0pt}) puissent
|
||
se faire à partir d'un seul point d'entrée.
|
||
|
||
L'utilité de ces fichiers est définie ci-dessous:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\texttt{settings.py} contient tous les paramètres globaux à notre
|
||
projet.
|
||
\item
|
||
\texttt{urls.py} contient les variables de routes, les adresses
|
||
utilisées et les fonctions vers lesquelles elles pointent.
|
||
\item
|
||
\texttt{manage.py}, pour toutes les commandes de gestion.
|
||
\item
|
||
\texttt{asgi.py} contient la définition de l'interface
|
||
\href{https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface}{ASGI},
|
||
le protocole pour la passerelle asynchrone entre votre application et
|
||
le serveur Web.
|
||
\item
|
||
\texttt{wsgi.py} contient la définition de l'interface
|
||
\href{https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface}{WSGI},
|
||
qui permettra à votre serveur Web (Nginx, Apache, \ldots\hspace{0pt})
|
||
de faire un pont vers votre projet.
|
||
\end{itemize}
|
||
|
||
Indiquer qu'il est possible d'avoir plusieurs structures de dossiers et
|
||
qu'il n'y a pas de "magie" derrière toutes ces commandes.
|
||
|
||
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel
|
||
nous stockerons les dépendances et un fichier README:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{(}\ExtensionTok{gwift}\KeywordTok{)}\NormalTok{ $ }\FunctionTok{mkdir}\NormalTok{ requirements}
|
||
\KeywordTok{(}\ExtensionTok{gwift}\KeywordTok{)}\NormalTok{ $ }\FunctionTok{touch}\NormalTok{ README.md}
|
||
\KeywordTok{(}\ExtensionTok{gwift}\KeywordTok{)}\NormalTok{ $ }\ExtensionTok{tree}\NormalTok{ gwift}
|
||
\ExtensionTok{gwift}
|
||
\NormalTok{├── }\ExtensionTok{gwift}
|
||
\NormalTok{│ ├── }\ExtensionTok{asgi.py}
|
||
\NormalTok{│ ├── }\ExtensionTok{\_\_init\_\_.py}
|
||
\NormalTok{│ ├── }\ExtensionTok{settings.py}
|
||
\NormalTok{│ ├── }\ExtensionTok{urls.py}
|
||
\NormalTok{│ └── }\ExtensionTok{wsgi.py}
|
||
\NormalTok{├── }\ExtensionTok{requirements}
|
||
\NormalTok{├── }\ExtensionTok{README.md}
|
||
\NormalTok{└── }\ExtensionTok{manage.py}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Ici
|
||
\item
|
||
Et là
|
||
\end{itemize}
|
||
|
||
Comme nous venons d'ajouter une dépendance à notre projet, profitons-en
|
||
pour créer un fichier reprenant tous les dépendances de notre projet.
|
||
Celles-ci sont normalement placées dans un fichier
|
||
\texttt{requirements.txt}. Dans un premier temps, ce fichier peut être
|
||
placé directement à la racine du projet, mais on préférera rapidement le
|
||
déplacer dans un sous-répertoire spécifique (\texttt{requirements}),
|
||
afin de grouper les dépendances en fonction de leur environnement de
|
||
destination:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\texttt{base.txt}
|
||
\item
|
||
\texttt{dev.txt}
|
||
\item
|
||
\texttt{production.txt}
|
||
\end{itemize}
|
||
|
||
Au début de chaque fichier, il suffit d'ajouter la ligne
|
||
\texttt{-r\ base.txt}, puis de lancer l'installation grâce à un
|
||
\texttt{pip\ install\ -r\ \textless{}nom\ du\ fichier\textgreater{}}. De
|
||
cette manière, il est tout à fait acceptable de n'installer
|
||
\texttt{flake8} et \texttt{django-debug-toolbar} qu'en développement par
|
||
exemple. Dans l'immédiat, nous allons ajouter \texttt{django} dans une
|
||
version strictement inférieure à la version 3.2 dans le fichier
|
||
\texttt{requirements/base.txt}.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\BuiltInTok{echo} \StringTok{\textquotesingle{}django==3.2\textquotesingle{}} \OperatorTok{\textgreater{}}\NormalTok{ requirements/base.txt}
|
||
\NormalTok{$ }\BuiltInTok{echo} \StringTok{\textquotesingle{}{-}r base.txt\textquotesingle{}} \OperatorTok{\textgreater{}}\NormalTok{ requirements/prod.txt}
|
||
\NormalTok{$ }\BuiltInTok{echo} \StringTok{\textquotesingle{}{-}r base.txt\textquotesingle{}} \OperatorTok{\textgreater{}}\NormalTok{ requirements/dev.txt}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Prenez directement l'habitude de spécifier la version ou les versions
|
||
compatibles: les librairies que vous utilisez comme dépendances
|
||
évoluent, de la même manière que vos projets. Pour être sûr et certain
|
||
le code que vous avez écrit continue à fonctionner, spécifiez la version
|
||
de chaque librairie de dépendances. Entre deux versions d'une même
|
||
librairie, des fonctions sont cassées, certaines signatures sont
|
||
modifiées, des comportements sont altérés, etc. Il suffit de parcourir
|
||
les pages de \emph{Changements incompatibles avec les anciennes versions
|
||
dans Django}
|
||
\href{https://docs.djangoproject.com/fr/3.1/releases/3.0/}{(par exemple
|
||
ici pour le passage de la 3.0 à la 3.1)} pour réaliser que certaines
|
||
opérations ne sont pas anodines, et que sans filet de sécurité, c'est le
|
||
mur assuré. Avec les mécanismes d'intégration continue et de tests
|
||
unitaires, nous verrons plus loin comment se prémunir d'un changement
|
||
inattendu.
|
||
|
||
\hypertarget{_gestion_des_diffuxe9rentes_versions_des_python}{%
|
||
\subsection{Gestion des différentes versions des
|
||
Python}\label{_gestion_des_diffuxe9rentes_versions_des_python}}
|
||
|
||
\begin{verbatim}
|
||
pyenv install 3.10
|
||
\end{verbatim}
|
||
|
||
\hypertarget{_django}{%
|
||
\subsection{Django}\label{_django}}
|
||
|
||
Comme nous l'avons vu ci-dessus, \texttt{django-admin} permet de créer
|
||
un nouveau projet. Nous faisons ici une distinction entre un
|
||
\textbf{projet} et une \textbf{application}:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\textbf{Un projet} représente l'ensemble des applications, paramètres,
|
||
pages HTML, middlewares, dépendances, etc., qui font que votre code
|
||
fait ce qu'il est sensé faire.
|
||
\item
|
||
\textbf{Une application} est un contexte d'exécution, idéalement
|
||
autonome, d'une partie du projet.
|
||
\end{itemize}
|
||
|
||
Pour \texttt{gwift}, nous aurons:
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/django-project-vs-apps-gwift.png}
|
||
\caption{Django Projet vs Applications}
|
||
\end{figure}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
une première application pour la gestion des listes de souhaits et des
|
||
éléments,
|
||
\item
|
||
une deuxième application pour la gestion des utilisateurs,
|
||
\item
|
||
voire une troisième application qui gérera les partages entre
|
||
utilisateurs et listes.
|
||
\end{enumerate}
|
||
|
||
Nous voyons également que la gestion des listes de souhaits et éléments
|
||
aura besoin de la gestion des utilisateurs - elle n'est pas autonome -,
|
||
tandis que la gestion des utilisateurs n'a aucune autre dépendance
|
||
qu'elle-même.
|
||
|
||
Pour \texttt{khana}, nous pourrions avoir quelque chose comme ceci:
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/django-project-vs-apps-khana.png}
|
||
\caption{Django Project vs Applications}
|
||
\end{figure}
|
||
|
||
En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la
|
||
gestion des utilisateurs et la possibilité qu'ils auront de communiquer
|
||
entre eux. Ceci pourrait être commun aux deux applications. Nous pouvons
|
||
clairement visualiser le principe de \textbf{contexte} pour une
|
||
application: celle-ci viendra avec son modèle, ses tests, ses vues et
|
||
son paramétrage et pourrait ainsi être réutilisée dans un autre projet.
|
||
C'est en ça que consistent les
|
||
\href{https://www.djangopackages.com/}{paquets Django} déjà disponibles:
|
||
ce sont "\emph{simplement}" de petites applications empaquetées et
|
||
pouvant être réutilisées dans différents contextes (eg.
|
||
\href{https://github.com/tomchristie/django-rest-framework}{Django-Rest-Framework},
|
||
\href{https://github.com/django-debug-toolbar/django-debug-toolbar}{Django-Debug-Toolbar},
|
||
\ldots\hspace{0pt}).
|
||
|
||
\hypertarget{_manage_py}{%
|
||
\subsubsection{manage.py}\label{_manage_py}}
|
||
|
||
Le fichier \texttt{manage.py} que vous trouvez à la racine de votre
|
||
projet est un \textbf{wrapper} sur les commandes \texttt{django-admin}.
|
||
A partir de maintenant, nous n'utiliserons plus que celui-là pour tout
|
||
ce qui touchera à la gestion de notre projet:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\texttt{manage.py\ check} pour vérifier (en surface\ldots\hspace{0pt})
|
||
que votre projet ne rencontre aucune erreur évidente
|
||
\item
|
||
\texttt{manage.py\ check\ -\/-deploy}, pour vérifier (en surface
|
||
aussi) que l'application est prête pour un déploiement
|
||
\item
|
||
\texttt{manage.py\ runserver} pour lancer un serveur de développement
|
||
\item
|
||
\texttt{manage.py\ test} pour découvrir les tests unitaires
|
||
disponibles et les lancer.
|
||
\end{itemize}
|
||
|
||
La liste complète peut être affichée avec \texttt{manage.py\ help}. Vous
|
||
remarquerez que ces commandes sont groupées selon différentes
|
||
catégories:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\textbf{auth}: création d'un nouveau super-utilisateur, changer le mot
|
||
de passe pour un utilisateur existant.
|
||
\item
|
||
\textbf{django}: vérifier la \textbf{compliance} du projet, lancer un
|
||
\textbf{shell}, \textbf{dumper} les données de la base, effectuer une
|
||
migration du schéma, \ldots\hspace{0pt}
|
||
\item
|
||
\textbf{sessions}: suppressions des sessions en cours
|
||
\item
|
||
\textbf{staticfiles}: gestion des fichiers statiques et lancement du
|
||
serveur de développement.
|
||
\end{itemize}
|
||
|
||
Nous verrons plus tard comment ajouter de nouvelles commandes.
|
||
|
||
Si nous démarrons la commande \texttt{python\ manage.py\ runserver},
|
||
nous verrons la sortie console suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py runserver}
|
||
\ExtensionTok{Watching}\NormalTok{ for file changes with StatReloader}
|
||
\ExtensionTok{Performing}\NormalTok{ system checks...}
|
||
|
||
\ExtensionTok{System}\NormalTok{ check identified no issues (0 silenced)}\ExtensionTok{.}
|
||
|
||
\NormalTok{[}\ExtensionTok{...}\NormalTok{]}
|
||
|
||
\ExtensionTok{December}\NormalTok{ 15, 2020 {-} 20:45:07}
|
||
\ExtensionTok{Django}\NormalTok{ version 3.1.4, using settings }\StringTok{\textquotesingle{}gwift.settings\textquotesingle{}}
|
||
\ExtensionTok{Starting}\NormalTok{ development server at http://127.0.0.1:8000/}
|
||
\ExtensionTok{Quit}\NormalTok{ the server with CTRL{-}BREAK.}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Si nous nous rendons sur la page \url{http://127.0.0.1:8000} (ou
|
||
\url{http://localhost:8000}) comme le propose si gentiment notre
|
||
(nouveau) meilleur ami, nous verrons ceci:
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/manage-runserver.png}
|
||
\caption{python manage.py runserver (Non, ce n'est pas Challenger)}
|
||
\end{figure}
|
||
|
||
Nous avons mis un morceau de la sortie console entre crochet
|
||
\texttt{{[}\ldots{}\hspace{0pt}{]}} ci-dessus, car elle concerne les
|
||
migrations. Si vous avez suivi les étapes jusqu'ici, vous avez également
|
||
dû voir un message type
|
||
\texttt{You\ have\ 18\ unapplied\ migration(s).\ {[}\ldots{}\hspace{0pt}{]}\ Run\ \textquotesingle{}python\ manage.py\ migrate\textquotesingle{}\ to\ apply\ them.}
|
||
Cela concerne les migrations, et c'est un point que nous verrons un peu
|
||
plus tard.
|
||
|
||
\hypertarget{_cruxe9ation_dune_nouvelle_application}{%
|
||
\subsubsection{Création d'une nouvelle
|
||
application}\label{_cruxe9ation_dune_nouvelle_application}}
|
||
|
||
Maintenant que nous avons a vu à quoi servait \texttt{manage.py}, nous
|
||
pouvons créer notre nouvelle application grâce à la commande
|
||
\texttt{manage.py\ startapp\ \textless{}label\textgreater{}}.
|
||
|
||
Notre première application servira à structurer les listes de souhaits,
|
||
les éléments qui les composent et les parties que chaque utilisateur
|
||
pourra offrir. De manière générale, essayez de trouver un nom éloquent,
|
||
court et qui résume bien ce que fait l'application. Pour nous, ce sera
|
||
donc \texttt{wish}.
|
||
|
||
C'est parti pour \texttt{manage.py\ startapp\ wish}!
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py startapp wish}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Résultat? Django nous a créé un répertoire \texttt{wish}, dans lequel
|
||
nous trouvons les fichiers et dossiers suivants:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\texttt{wish/init.py} pour que notre répertoire \texttt{wish} soit
|
||
converti en package Python.
|
||
\item
|
||
\texttt{wish/admin.py} servira à structurer l'administration de notre
|
||
application. Chaque information peut être administrée facilement au
|
||
travers d'une interface générée à la volée par le framework. Nous y
|
||
reviendrons par la suite.
|
||
\item
|
||
\texttt{wish/apps.py} qui contient la configuration de l'application
|
||
et qui permet notamment de fixer un nom ou un libellé
|
||
\url{https://docs.djangoproject.com/en/stable/ref/applications/}
|
||
\item
|
||
\texttt{wish/migrations/} est le dossier dans lequel seront stockées
|
||
toutes les différentes migrations de notre application (= toutes les
|
||
modifications que nous apporterons aux données que nous souhaiterons
|
||
manipuler)
|
||
\item
|
||
\texttt{wish/models.py} représentera et structurera nos données, et
|
||
est intimement lié aux migrations.
|
||
\item
|
||
\texttt{wish/tests.py} pour les tests unitaires.
|
||
\end{itemize}
|
||
|
||
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire
|
||
\texttt{wish} dans votre répertoire \texttt{gwift} existant. C'est une
|
||
forme de convention.
|
||
|
||
La structure de vos répertoires devient celle-ci:
|
||
|
||
\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}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Notre application a bien été créée, et nous l'avons déplacée dans le
|
||
répertoire \texttt{gwift} !
|
||
\end{itemize}
|
||
|
||
\hypertarget{_fonctionement_guxe9nuxe9ral}{%
|
||
\subsubsection{Fonctionement
|
||
général}\label{_fonctionement_guxe9nuxe9ral}}
|
||
|
||
Le métier de programmeur est devenu de plus en plus complexe. Il y a 20
|
||
ans, nous pouvions nous contenter d'une simple page PHP dans laquelle
|
||
nous mixions l'ensemble des actios à réaliser: requêtes en bases de
|
||
données, construction de la page, \ldots\hspace{0pt} La recherche d'une
|
||
solution a un problème n'était pas spécialement plus complexe - dans la
|
||
mesure où le rendu des enregistrements en direct n'était finalement
|
||
qu'une forme un chouia plus évoluée du \texttt{print()} ou des
|
||
\texttt{System.out.println()} - mais c'était l'évolutivité des
|
||
applications qui en prenait un coup: une grosse partie des tâches
|
||
étaient dupliquées entre les différentes pages, et l'ajout d'une
|
||
nouvelle fonctionnalité était relativement ardue.
|
||
|
||
Django (et d'autres cadriciels) résolvent ce problème en se basant
|
||
ouvertement sur le principe de \texttt{Don’t\ repeat\ yourself}
|
||
\footnote{DRY}. Chaque morceau de code ne doit apparaitre qu'une seule
|
||
fois, afin de limiter au maximum la redite (et donc, l'application d'un
|
||
même correctif à différents endroits).
|
||
|
||
Le chemin parcouru par une requête est expliqué en (petits) détails
|
||
ci-dessous.
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/diagrams/django-how-it-works.png}
|
||
\caption{How it works}
|
||
\end{figure}
|
||
|
||
\textbf{1. Un utilisateur ou un visiteur souhaite accéder à une URL
|
||
hébergée et servie par notre application}. Ici, nous prenons l'exemple
|
||
de l'URL fictive \texttt{https://gwift/wishes/91827}. Lorsque cette URL
|
||
"arrive" dans notre application, son point d'entrée se trouvera au
|
||
niveau des fichiers \texttt{asgi.py} ou \texttt{wsgi.py}. Nous verrons
|
||
cette partie plus tard, et nous pouvons nous concentrer sur le chemin
|
||
interne qu'elle va parcourir.
|
||
|
||
\textbf{Etape 0} - La première étape consiste à vérifier que cette URL
|
||
répond à un schéma que nous avons défini dans le fichier
|
||
\texttt{gwift/urls.py}.
|
||
|
||
\textbf{Etape 1} - Si ce n'est pas le cas, l'application n'ira pas plus
|
||
loin et retournera une erreur à l'utilisateur.
|
||
|
||
\textbf{Etape 2} - Django va parcourir l'ensemble des \emph{patterns}
|
||
présents dans le fichier \texttt{urls.py} et s'arrêtera sur le premier
|
||
qui correspondra à la requête qu'il a reçue. Ce cas est relativement
|
||
trivial: la requête \texttt{/wishes/91827} a une correspondance au
|
||
niveau de la ligne
|
||
\texttt{path("wishes/\textless{}int:wish\_id\textgreater{}} dans
|
||
l'exemple ci-dessous. Django va alors appeler la fonction \footnote{Qui
|
||
ne sera pas toujours une fonction. Django s'attend à trouver un
|
||
\emph{callable}, c'est-à-dire n'importe quel élément qu'il peut
|
||
appeler comme une fonction.} associée à ce \emph{pattern},
|
||
c'est-à-dire \texttt{wish\_details} du module \texttt{gwift.views}.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
|
||
\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path}
|
||
|
||
\ImportTok{from}\NormalTok{ gwift.views }\ImportTok{import}\NormalTok{ wish\_details }
|
||
|
||
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls),}
|
||
\NormalTok{ path(}\StringTok{"wishes/\textless{}int:wish\_id\textgreater{}"}\NormalTok{, wish\_details), }
|
||
\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Nous importons la fonction \texttt{wish\_details} du module
|
||
\texttt{gwift.views}
|
||
\item
|
||
Champomy et cotillons! Nous avons une correspondance avec
|
||
\texttt{wishes/details/91827}
|
||
\end{itemize}
|
||
|
||
TODO: En fait, il faudrait quand même s'occuper du modèle ici. TODO: et
|
||
de la mise en place de l'administration, parce que nous en aurons besoin
|
||
pour les étapes de déploiement.
|
||
|
||
Nous n'allons pas nous occuper de l'accès à la base de données pour le
|
||
moment (nous nous en occuperons dans un prochain chapitre) et nous nous
|
||
contenterons de remplir un canevas avec un ensemble de données.
|
||
|
||
Le module \texttt{gwift.views} qui se trouve dans le fichier
|
||
\texttt{gwift/views.py} peut ressembler à ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{[...]}
|
||
|
||
\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ datetime}
|
||
|
||
|
||
\KeywordTok{def}\NormalTok{ wishes\_details(request: HttpRequest, wish\_id: }\BuiltInTok{int}\NormalTok{) }\OperatorTok{{-}\textgreater{}}\NormalTok{ HttpResponse:}
|
||
\NormalTok{ context }\OperatorTok{=}\NormalTok{ \{}
|
||
\StringTok{"user\_name"}\NormalTok{: }\StringTok{"Bond,"}
|
||
\StringTok{"user\_first\_name"}\NormalTok{: }\StringTok{"James"}\NormalTok{,}
|
||
\StringTok{"now"}\NormalTok{: datetime.now()}
|
||
\NormalTok{ \}}
|
||
|
||
\ControlFlowTok{return}\NormalTok{ render(}
|
||
\NormalTok{ request,}
|
||
\StringTok{"wish\_details.html"}\NormalTok{,}
|
||
\NormalTok{ context}
|
||
\NormalTok{ )}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Pour résumer, cette fonction permet:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
De construire un \emph{contexte}, qui est représenté sous la forme
|
||
d'un dictionnaire associant des clés à des valeurs. Les clés sont
|
||
respectivement \texttt{user\_name}, \texttt{user\_first\_name} et
|
||
\texttt{now}, tandis que leurs valeurs respectives sont \texttt{Bond},
|
||
\texttt{James} et le \texttt{moment\ présent} \footnote{Non, pas celui
|
||
d'Eckhart Tolle}.
|
||
\item
|
||
Nous passons ensuite ce dictionnaire à un canevas,
|
||
\texttt{wish\_details.html}
|
||
\item
|
||
L'application du contexte sur le canevas nous donne un résultat.
|
||
\end{enumerate}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\textless{}!{-}{-} fichier wish\_details.html {-}{-}\textgreater{}}
|
||
\DataTypeTok{\textless{}!DOCTYPE }\NormalTok{html}\DataTypeTok{\textgreater{}}
|
||
\KeywordTok{\textless{}html\textgreater{}}
|
||
\KeywordTok{\textless{}head\textgreater{}}
|
||
\KeywordTok{\textless{}title\textgreater{}}\NormalTok{Page title}\KeywordTok{\textless{}/title\textgreater{}}
|
||
\KeywordTok{\textless{}/head\textgreater{}}
|
||
\KeywordTok{\textless{}body\textgreater{}}
|
||
\KeywordTok{\textless{}h1\textgreater{}}\NormalTok{👤 Hi!}\KeywordTok{\textless{}/h1\textgreater{}}
|
||
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{My name is \{\{ user\_name \}\}. \{\{ user\_first\_name \}\} \{\{ user\_name \}\}.}\KeywordTok{\textless{}/p\textgreater{}}
|
||
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{This page was generated at \{\{ now \}\}}\KeywordTok{\textless{}/p\textgreater{}}
|
||
\KeywordTok{\textless{}/body\textgreater{}}
|
||
\KeywordTok{\textless{}/html\textgreater{}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Après application de notre contexte sur ce template, nous obtiendrons ce
|
||
document, qui sera renvoyé au navigateur de l'utilisateur qui aura fait
|
||
la requête initiale:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\DataTypeTok{\textless{}!DOCTYPE }\NormalTok{html}\DataTypeTok{\textgreater{}}
|
||
\KeywordTok{\textless{}html\textgreater{}}
|
||
\KeywordTok{\textless{}head\textgreater{}}
|
||
\KeywordTok{\textless{}title\textgreater{}}\NormalTok{Page title}\KeywordTok{\textless{}/title\textgreater{}}
|
||
\KeywordTok{\textless{}/head\textgreater{}}
|
||
\KeywordTok{\textless{}body\textgreater{}}
|
||
\KeywordTok{\textless{}h1\textgreater{}}\NormalTok{👤 Hi!}\KeywordTok{\textless{}/h1\textgreater{}}
|
||
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{My name is Bond. James Bond.}\KeywordTok{\textless{}/p\textgreater{}}
|
||
\KeywordTok{\textless{}p\textgreater{}}\NormalTok{This page was generated at 2027{-}03{-}19 19:47:38}\KeywordTok{\textless{}/p\textgreater{}}
|
||
\KeywordTok{\textless{}/body\textgreater{}}
|
||
\KeywordTok{\textless{}/html\textgreater{}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/django-first-template.png}
|
||
\caption{Résultat}
|
||
\end{figure}
|
||
|
||
\hypertarget{_12_facteurs_et_configuration_globale}{%
|
||
\subsubsection{12 facteurs et configuration
|
||
globale}\label{_12_facteurs_et_configuration_globale}}
|
||
|
||
→ Faire le lien avec les settings → Faire le lien avec les douze
|
||
facteurs → Construction du fichier setup.cfg
|
||
|
||
\hypertarget{_setup_cfg}{%
|
||
\subsubsection{setup.cfg}\label{_setup_cfg}}
|
||
|
||
(Repris de cookie-cutter-django)
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{[flake8]}
|
||
\DataTypeTok{max{-}line{-}length }\OtherTok{=}\StringTok{ }\DecValTok{120}
|
||
\DataTypeTok{exclude }\OtherTok{=}\StringTok{ .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node\_modules,venv}
|
||
|
||
\KeywordTok{[pycodestyle]}
|
||
\DataTypeTok{max{-}line{-}length }\OtherTok{=}\StringTok{ }\DecValTok{120}
|
||
\DataTypeTok{exclude }\OtherTok{=}\StringTok{ .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node\_modules,venv}
|
||
|
||
\KeywordTok{[mypy]}
|
||
\DataTypeTok{python\_version }\OtherTok{=}\StringTok{ }\FloatTok{3.8}
|
||
\DataTypeTok{check\_untyped\_defs }\OtherTok{=}\StringTok{ }\KeywordTok{True}
|
||
\DataTypeTok{ignore\_missing\_imports }\OtherTok{=}\StringTok{ }\KeywordTok{True}
|
||
\DataTypeTok{warn\_unused\_ignores }\OtherTok{=}\StringTok{ }\KeywordTok{True}
|
||
\DataTypeTok{warn\_redundant\_casts }\OtherTok{=}\StringTok{ }\KeywordTok{True}
|
||
\DataTypeTok{warn\_unused\_configs }\OtherTok{=}\StringTok{ }\KeywordTok{True}
|
||
\DataTypeTok{plugins }\OtherTok{=}\StringTok{ mypy\_django\_plugin.main}
|
||
|
||
\KeywordTok{[mypy.plugins.django{-}stubs]}
|
||
\DataTypeTok{django\_settings\_module }\OtherTok{=}\StringTok{ config.settings.test}
|
||
|
||
\KeywordTok{[mypy{-}*.migrations.*]}
|
||
\CommentTok{\# Django migrations should not produce any errors:}
|
||
\DataTypeTok{ignore\_errors }\OtherTok{=}\StringTok{ }\KeywordTok{True}
|
||
|
||
\KeywordTok{[coverage:run]}
|
||
\DataTypeTok{include }\OtherTok{=}\StringTok{ khana/*}
|
||
\DataTypeTok{omit }\OtherTok{=}\StringTok{ *migrations*, *tests*}
|
||
\DataTypeTok{plugins }\OtherTok{=}
|
||
\DataTypeTok{ django\_coverage\_plugin}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_structure_finale_de_notre_environnement}{%
|
||
\subsection{Structure finale de notre
|
||
environnement}\label{_structure_finale_de_notre_environnement}}
|
||
|
||
Nous avons donc la structure finale pour notre environnement de travail:
|
||
|
||
\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{_cookie_cutter}{%
|
||
\subsection{Cookie cutter}\label{_cookie_cutter}}
|
||
|
||
Pfiou! Ca en fait des commandes et du boulot pour "juste" démarrer un
|
||
nouveau projet, non? Sachant qu'en plus, nous avons dû modifier des
|
||
fichiers, déplacer des dossiers, ajouter des dépendances, configurer une
|
||
base de données, \ldots\hspace{0pt}
|
||
|
||
Bonne nouvelle! Il existe des générateurs, permettant de démarrer
|
||
rapidement un nouveau projet sans (trop) se prendre la tête. Le plus
|
||
connu (et le plus personnalisable) est
|
||
\href{https://cookiecutter.readthedocs.io/}{Cookie-Cutter}, qui se base
|
||
sur des canevas \emph{type
|
||
\href{https://pypi.org/project/Jinja2/}{Jinja2}}, pour créer une
|
||
arborescence de dossiers et fichiers conformes à votre manière de
|
||
travailler. Et si vous avez la flemme de créer votre propre canevas,
|
||
vous pouvez utiliser
|
||
\href{https://cookiecutter-django.readthedocs.io}{ceux qui existent
|
||
déjà}.
|
||
|
||
Pour démarrer, créez un environnement virtuel (comme d'habitude):
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{λ }\ExtensionTok{python}\NormalTok{ {-}m venv .venvs\textbackslash{}cookie{-}cutter{-}khana}
|
||
\NormalTok{λ }\ExtensionTok{.venvs}\NormalTok{\textbackslash{}cookie{-}cutter{-}khana\textbackslash{}Scripts\textbackslash{}activate.bat}
|
||
\KeywordTok{(}\ExtensionTok{cookie{-}cutter{-}khana}\KeywordTok{)}\NormalTok{ λ }\ExtensionTok{pip}\NormalTok{ install cookiecutter}
|
||
|
||
\ExtensionTok{Collecting}\NormalTok{ cookiecutter}
|
||
\NormalTok{ [}\ExtensionTok{...}\NormalTok{]}
|
||
\ExtensionTok{Successfully}\NormalTok{ installed Jinja2{-}2.11.2 MarkupSafe{-}1.1.1 arrow{-}0.17.0 binaryornot{-}0.4.4 certifi{-}2020.12.5 chardet{-}4.0.0 click{-}7.1.2 cookiecutter{-}1.7.2 idna{-}2.10 jinja2{-}time{-}0.2.0 poyo{-}0.5.0 python{-}dateutil{-}2.8.1 python{-}slugify{-}4.0.1 requests{-}2.25.1 six{-}1.15.0 text{-}unidecode{-}1.3 urllib3{-}1.26.2}
|
||
|
||
\KeywordTok{(}\ExtensionTok{cookie{-}cutter{-}khana}\KeywordTok{)}\NormalTok{ λ }\ExtensionTok{cookiecutter}\NormalTok{ https://github.com/pydanny/cookiecutter{-}django}
|
||
|
||
\NormalTok{ [}\ExtensionTok{...}\NormalTok{]}
|
||
|
||
\NormalTok{ [}\ExtensionTok{SUCCESS}\NormalTok{]: Project initialized, keep up the good work!}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Si vous explorez les différents fichiers, vous trouverez beaucoup de
|
||
similitudes avec la configuration que nous vous proposions ci-dessus. En
|
||
fonction de votre expérience, vous serez tenté de modifier certains
|
||
paramètres, pour faire correspondre ces sources avec votre utilisation
|
||
ou vos habitudes.
|
||
|
||
Il est aussi possible d'utiliser l'argument \texttt{-\/-template},
|
||
suivie d'un argument reprenant le nom de votre projet
|
||
(\texttt{\textless{}my\_project\textgreater{}}), lors de
|
||
l'initialisation d'un projet avec la commande \texttt{startproject} de
|
||
\texttt{django-admin}, afin de calquer votre arborescence sur un projet
|
||
existant. La
|
||
\href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startproject}{documentation}
|
||
à ce sujet est assez complète.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ExtensionTok{django{-}admin.py}\NormalTok{ startproject {-}{-}template=https://[...].zip }\OperatorTok{\textless{}}\NormalTok{my\_project}\OperatorTok{\textgreater{}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Dans ce chapitre, nous allons parler de plusieurs concepts fondamentaux
|
||
au développement rapide d'une application utilisant Django. Nous
|
||
parlerons de modélisation, de métamodèle, de migrations,
|
||
d'administration auto-générée, de traductions et de cycle de vie des
|
||
données.
|
||
|
||
Django est un framework Web qui propose une très bonne intégration des
|
||
composants et une flexibilité bien pensée: chacun des composants permet
|
||
de définir son contenu de manière poussée, en respectant des contraintes
|
||
logiques et faciles à retenir, et en gérant ses dépendances de manière
|
||
autonome. Pour un néophyte, la courbe d'apprentissage sera relativement
|
||
ardue: à côté de concepts clés de Django, il conviendra également
|
||
d'assimiler correctement les structures de données du langage Python, le
|
||
cycle de vie des requêtes HTTP et le B.A-BA des principes de sécurité.
|
||
|
||
En restant dans les sentiers battus, votre projet suivra un patron de
|
||
conception dérivé du modèle \texttt{MVC} (Modèle-Vue-Controleur), où la
|
||
variante concerne les termes utilisés: Django les nomme respectivement
|
||
Modèle-Template-Vue et leur contexte d'utilisation. Dans un
|
||
\textbf{pattern} MVC classique, la traduction immédiate du
|
||
\textbf{contrôleur} est une \textbf{vue}. Et comme nous le verrons par
|
||
la suite, la \textbf{vue} est en fait le \textbf{template}. La
|
||
principale différence avec un modèle MVC concerne le fait que la vue ne
|
||
s'occupe pas du routage des URLs; ce point est réalisé par un autre
|
||
composant, interne au framework, graĉe aux différentes routes définies
|
||
dans les fichiers \texttt{urls.py}.
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Le \textbf{modèle} (\texttt{models.py}) fait le lien avec la base de
|
||
données et permet de définir les champs et leur type à associer à une
|
||
table. \emph{Grosso modo}*, une table SQL correspondra à une classe
|
||
d'un modèle Django.
|
||
\item
|
||
La \textbf{vue} (\texttt{views.py}), qui joue le rôle de contrôleur:
|
||
\emph{a priori}, tous les traitements, la récupération des données,
|
||
etc. doit passer par ce composant et ne doit (pratiquement) pas être
|
||
généré à la volée, directement à l'affichage d'une page. En d'autres
|
||
mots, la vue sert de pont entre les données gérées par la base et
|
||
l'interface utilisateur.
|
||
\item
|
||
Le \textbf{template}, qui s'occupe de la mise en forme: c'est le
|
||
composant qui s'occupe de transformer les données en un affichage
|
||
compréhensible (avec l'aide du navigateur) pour l'utilisateur.
|
||
\end{itemize}
|
||
|
||
Pour reprendre une partie du schéma précédent, lorsqu'une requête est
|
||
émise par un utilisateur, la première étape va consister à trouver une
|
||
\emph{route} qui correspond à cette requête, c'est à dire à trouver la
|
||
correspondance entre l'URL qui est demandée par l'utilisateur et la
|
||
fonction du langage qui sera exécutée pour fournir le résultat attendu.
|
||
Cette fonction correspond au \textbf{contrôleur} et s'occupera de
|
||
construire le \textbf{modèle} correspondant.
|
||
|
||
\hypertarget{_moduxe9lisation}{%
|
||
\section{Modélisation}\label{_moduxe9lisation}}
|
||
|
||
Ce chapitre aborde la modélisation des objets et les options qui y sont
|
||
liées.
|
||
|
||
Avec Django, la modélisation est en lien direct avec la conception et le
|
||
stockage, sous forme d'une base de données relationnelle, et la manière
|
||
dont ces données s'agencent et communiquent entre elles. Cette
|
||
modélisation va ériger les premières pierres de votre édifice
|
||
|
||
\begin{quote}
|
||
\emph{Le modèle n'est qu'une grande hypothèque. Il se base sur des choix
|
||
conscients et inconscients, et dans chacun de ces choix se cachent nos
|
||
propres perceptions qui résultent de qui nous sommes, de nos
|
||
connaissances, de nos profils scientifiques et de tant d'autres choses.}
|
||
|
||
--- Aurélie Jean De l'autre côté de la machine
|
||
\end{quote}
|
||
|
||
Comme expliqué par Aurélie Jean cite:{[}other\_side{]}, "\emph{toute
|
||
modélisation reste une approximation de la réalité}". Plus tard dans ce
|
||
chapitre, nous expliquerons les bonnes pratiques à suivre pour faire
|
||
évoluer ces biais.
|
||
|
||
Django utilise un paradigme de persistence des données de type
|
||
\href{https://fr.wikipedia.org/wiki/Mapping_objet-relationnel}{ORM} -
|
||
c'est-à-dire que chaque type d'objet manipulé peut s'apparenter à une
|
||
table SQL, tout en respectant une approche propre à la programmation
|
||
orientée object. Plus spécifiquement, l'ORM de Django suit le patron de
|
||
conception
|
||
\href{https://en.wikipedia.org/wiki/Active_record_pattern}{Active
|
||
Records}, comme le font par exemple
|
||
\href{https://rubyonrails.org/}{Rails} pour Ruby ou
|
||
\href{https://docs.microsoft.com/fr-fr/ef/}{EntityFramework} pour .Net.
|
||
|
||
Le modèle de données de Django est sans doute la (seule ?) partie qui
|
||
soit tellement couplée au framework qu'un changement à ce niveau
|
||
nécessitera une refonte complète de beaucoup d'autres briques de vos
|
||
applications; là où un pattern de type
|
||
\href{https://www.martinfowler.com/eaaCatalog/repository.html}{Repository}
|
||
permettrait justement de découpler le modèle des données de l'accès à
|
||
ces mêmes données, un pattern Active Record lie de manière extrêmement
|
||
forte le modèle à sa persistence. Architecturalement, c'est sans doute
|
||
la plus grosse faiblesse de Django, à tel point que \textbf{ne pas
|
||
utiliser cette brique de fonctionnalités} peut remettre en question le
|
||
choix du framework.
|
||
|
||
Conceptuellement, c'est pourtant la manière de faire qui permettra
|
||
d'avoir quelque chose à présenter très rapidement: à partir du moment où
|
||
vous aurez un modèle de données, vous aurez accès, grâce à cet ORM à:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Des migrations de données et la possibilité de faire évoluer votre
|
||
modèle,
|
||
\item
|
||
Une abstraction entre votre modélisation et la manière dont les
|
||
données sont représentées \emph{via} un moteur de base de données
|
||
relationnelles,
|
||
\item
|
||
Une interface d'administration auto-générée
|
||
\item
|
||
Un mécanisme de formulaires HTML qui soit complet, pratique à
|
||
utiliser, orienté objet et facile à faire évoluer,
|
||
\item
|
||
Une définition des notions d'héritage (tout en restant dans une forme
|
||
d'héritage simple).
|
||
\end{enumerate}
|
||
|
||
Comme tout ceci reste au niveau du code, cela suit également la
|
||
méthodologie des douze facteurs, concernant la minimisation des
|
||
divergences entre environnements d'exécution: comme tout se trouve au
|
||
niveau du code, il n'est plus nécessaire d'avoir un DBA qui doive
|
||
démarrer un script sur un serveur au moment de la mise à jour, de
|
||
recevoir une release note de 512 pages en PDF reprenant les
|
||
modifications ou de nécessiter l'intervention de trois équipes
|
||
différentes lors d'une modification majeure du code. Déployer une
|
||
nouvelle instance de l'application pourra être réalisé directement à
|
||
partir d'une seule et même commande.
|
||
|
||
\hypertarget{_active_records}{%
|
||
\subsection{Active Records}\label{_active_records}}
|
||
|
||
Il est important de noter que l'implémentation d'Active Records reste
|
||
une forme hybride entre une structure de données brutes et une classe:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Une classe va exposer ses données derrière une forme d'abstraction et
|
||
n'exposer que les fonctions qui opèrent sur ces données,
|
||
\item
|
||
Une structure de données ne va exposer que ses champs et propriétés,
|
||
et ne va pas avoir de functions significatives.
|
||
\end{itemize}
|
||
|
||
L'exemple ci-dessous présente trois structure de données, qui exposent
|
||
chacune leurs propres champs:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Square:}
|
||
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, side):}
|
||
\VariableTok{self}\NormalTok{.top\_left }\OperatorTok{=}\NormalTok{ top\_left}
|
||
\VariableTok{self}\NormalTok{.side }\OperatorTok{=}\NormalTok{ side}
|
||
|
||
\KeywordTok{class}\NormalTok{ Rectangle:}
|
||
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, height, width):}
|
||
\VariableTok{self}\NormalTok{.top\_left }\OperatorTok{=}\NormalTok{ top\_left}
|
||
\VariableTok{self}\NormalTok{.height }\OperatorTok{=}\NormalTok{ height}
|
||
\VariableTok{self}\NormalTok{.width }\OperatorTok{=}\NormalTok{ width}
|
||
|
||
\KeywordTok{class}\NormalTok{ Circle:}
|
||
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, center, radius):}
|
||
\VariableTok{self}\NormalTok{.center }\OperatorTok{=}\NormalTok{ center}
|
||
\VariableTok{self}\NormalTok{.radius }\OperatorTok{=}\NormalTok{ radius}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Si nous souhaitons ajouter une fonctionnalité permettant de calculer
|
||
l'aire pour chacune de ces structures, nous aurons deux possibilités:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Soit ajouter une classe de \emph{visite} qui ajoute cette fonction de
|
||
calcul d'aire
|
||
\item
|
||
Soit modifier notre modèle pour que chaque structure hérite d'une
|
||
classe de type \texttt{Shape}, qui implémentera elle-même ce calcul
|
||
d'aire.
|
||
\end{enumerate}
|
||
|
||
Dans le premier cas, nous pouvons procéder de la manière suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Geometry:}
|
||
\NormalTok{ PI }\OperatorTok{=} \FloatTok{3.141592653589793}
|
||
|
||
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{, shape):}
|
||
\ControlFlowTok{if} \BuiltInTok{isinstance}\NormalTok{(shape, Square):}
|
||
\ControlFlowTok{return}\NormalTok{ shape.side }\OperatorTok{*}\NormalTok{ shape.side}
|
||
|
||
\ControlFlowTok{if} \BuiltInTok{isinstance}\NormalTok{(shape, Rectangle):}
|
||
\ControlFlowTok{return}\NormalTok{ shape.height }\OperatorTok{*}\NormalTok{ shape.width}
|
||
|
||
\ControlFlowTok{if} \BuiltInTok{isinstance}\NormalTok{(shape, Circle):}
|
||
\ControlFlowTok{return}\NormalTok{ PI }\OperatorTok{*}\NormalTok{ shape.radius}\OperatorTok{**}\DecValTok{2}
|
||
|
||
\ControlFlowTok{raise}\NormalTok{ NoSuchShapeException()}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Dans le second cas, l'implémentation pourrait évoluer de la manière
|
||
suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Shape:}
|
||
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{pass}
|
||
|
||
\KeywordTok{class}\NormalTok{ Square(Shape):}
|
||
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, side):}
|
||
\VariableTok{self}\NormalTok{.\_\_top\_left }\OperatorTok{=}\NormalTok{ top\_left}
|
||
\VariableTok{self}\NormalTok{.\_\_side }\OperatorTok{=}\NormalTok{ side}
|
||
|
||
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.\_\_side }\OperatorTok{*} \VariableTok{self}\NormalTok{.\_\_side}
|
||
|
||
\KeywordTok{class}\NormalTok{ Rectangle(Shape):}
|
||
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, top\_left, height, width):}
|
||
\VariableTok{self}\NormalTok{.\_\_top\_left }\OperatorTok{=}\NormalTok{ top\_left}
|
||
\VariableTok{self}\NormalTok{.\_\_height }\OperatorTok{=}\NormalTok{ height}
|
||
\VariableTok{self}\NormalTok{.\_\_width }\OperatorTok{=}\NormalTok{ width}
|
||
|
||
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{return} \VariableTok{self}\NormalTok{.\_\_height }\OperatorTok{*} \VariableTok{self}\NormalTok{.\_\_width}
|
||
|
||
\KeywordTok{class}\NormalTok{ Circle(Shape):}
|
||
\KeywordTok{def} \FunctionTok{\_\_init\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{, center, radius):}
|
||
\VariableTok{self}\NormalTok{.\_\_center }\OperatorTok{=}\NormalTok{ center}
|
||
\VariableTok{self}\NormalTok{.\_\_radius }\OperatorTok{=}\NormalTok{ radius}
|
||
|
||
\KeywordTok{def}\NormalTok{ area(}\VariableTok{self}\NormalTok{):}
|
||
\NormalTok{ PI }\OperatorTok{=} \FloatTok{3.141592653589793}
|
||
\ControlFlowTok{return}\NormalTok{ PI }\OperatorTok{*} \VariableTok{self}\NormalTok{.\_\_radius}\OperatorTok{**}\DecValTok{2}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Une structure de données peut être rendue abstraite au travers des
|
||
notions de programmation orientée objet.
|
||
|
||
Dans l'exemple géométrique ci-dessus, repris de
|
||
cite:{[}clean\_code(95-97){]}, l'accessibilité des champs devient
|
||
restreinte, tandis que la fonction \texttt{area()} bascule comme méthode
|
||
d'instance plutôt que de l'isoler au niveau d'un visiteur. Nous ajoutons
|
||
une abstraction au niveau des formes grâce à un héritage sur la classe
|
||
\texttt{Shape}; indépendamment de ce que nous manipulerons, nous aurons
|
||
la possibilité de calculer son aire.
|
||
|
||
Une structure de données permet de facilement gérer des champs et des
|
||
propriétés, tandis qu'une classe gère et facilite l'ajout de fonctions
|
||
et de méthodes.
|
||
|
||
Le problème d'Active Records est que chaque classe s'apparente à une
|
||
table SQL et revient donc à gérer des \emph{DTO} ou \emph{Data Transfer
|
||
Object}, c'est-à-dire des objets de correspondance pure et simple entre
|
||
les champs de la base de données et les propriétés de la programmation
|
||
orientée objet, c'est-à-dire également des classes sans fonctions. Or,
|
||
chaque classe a également la possibilité d'exposer des possibilités
|
||
d'interactions au niveau de la persistence, en
|
||
\href{https://docs.djangoproject.com/en/stable/ref/models/instances/\#django.db.models.Model.save}{enregistrant
|
||
ses propres données} ou en en autorisant leur
|
||
\href{https://docs.djangoproject.com/en/stable/ref/models/instances/\#deleting-objects}{suppression}.
|
||
Nous arrivons alors à un modèle hybride, mélangeant des structures de
|
||
données et des classes d'abstraction, ce qui restera parfaitement viable
|
||
tant que l'on garde ces principes en tête et que l'on se prépare à une
|
||
éventuelle réécriture du code.
|
||
|
||
Lors de l'analyse d'une classe de modèle, nous pouvons voir que Django
|
||
exige un héritage de la classe \texttt{django.db.models.Model}. Nous
|
||
pouvons regarder les propriétés définies dans cette classe en analysant
|
||
le fichier
|
||
\texttt{lib\textbackslash{}site-packages\textbackslash{}django\textbackslash{}models\textbackslash{}base.py}.
|
||
Outre que \texttt{models.Model} hérite de \texttt{ModelBase} au travers
|
||
de \href{https://pypi.python.org/pypi/six}{six} pour la
|
||
rétrocompatibilité vers Python 2.7, cet héritage apporte notamment les
|
||
fonctions \texttt{save()}, \texttt{clean()}, \texttt{delete()},
|
||
\ldots\hspace{0pt} En résumé, toutes les méthodes qui font qu'une
|
||
instance sait \textbf{comment} interagir avec la base de données.
|
||
|
||
\hypertarget{_types_de_champs_relations_et_cluxe9s_uxe9tranguxe8res}{%
|
||
\subsection{Types de champs, relations et clés
|
||
étrangères}\label{_types_de_champs_relations_et_cluxe9s_uxe9tranguxe8res}}
|
||
|
||
Nous l'avons vu plus tôt, Python est un langage dynamique et fortement
|
||
typé. Django, de son côté, ajoute une couche de typage statique exigé
|
||
par le lien sous-jacent avec le moteur de base de données relationnelle.
|
||
Dans le domaine des bases de données relationnelles, un point
|
||
d'attention est de toujours disposer d'une clé primaire pour nos
|
||
enregistrements. Si aucune clé primaire n'est spécifiée, Django
|
||
s'occupera d'en ajouter une automatiquement et la nommera (par
|
||
convention) \texttt{id}. Elle sera ainsi accessible autant par cette
|
||
propriété que par la propriété \texttt{pk}.
|
||
|
||
Chaque champ du modèle est donc typé et lié, soit à une primitive, soit
|
||
à une autre instance au travers de sa clé d'identification.
|
||
|
||
Grâce à toutes ces informations, nous sommes en mesure de représenter
|
||
facilement des livres liés à des catégories:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Category(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
\KeywordTok{class}\NormalTok{ Book(models.Model):}
|
||
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ForeignKey(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Par défaut, et si aucune propriété ne dispose d'un attribut
|
||
\texttt{primary\_key=True}, Django s'occupera d'ajouter un champ
|
||
\texttt{id} grâce à son héritage de la classe \texttt{models.Model}. Les
|
||
autres champs nous permettent d'identifier une catégorie
|
||
(\texttt{Category}) par un nom (\texttt{name}), tandis qu'un livre
|
||
(\texttt{Book}) le sera par ses propriétés \texttt{title} et une clé de
|
||
relation vers une catégorie. Un livre est donc lié à une catégorie,
|
||
tandis qu'une catégorie est associée à plusieurs livres.
|
||
|
||
\includegraphics{diagrams/books-foreign-keys-example.drawio.png}
|
||
|
||
A présent que notre structure dispose de sa modélisation, il nous faut
|
||
informer le moteur de base de données de créer la structure
|
||
correspondance:
|
||
|
||
\begin{verbatim}
|
||
$ python manage.py makemigrations
|
||
Migrations for 'library':
|
||
library/migrations/0001_initial.py
|
||
- Create model Category
|
||
- Create model Book
|
||
\end{verbatim}
|
||
|
||
Cette étape créera un fichier différentiel, explicitant les
|
||
modifications à appliquer à la structure de données pour rester en
|
||
corrélation avec la modélisation de notre application.
|
||
|
||
Nous pouvons écrire un premier code d'initialisation de la manière
|
||
suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ library.models }\ImportTok{import}\NormalTok{ Book, Category}
|
||
|
||
\NormalTok{movies }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Adaptations au cinéma"}\NormalTok{)}
|
||
\NormalTok{medieval }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Médiéval{-}Fantastique"}\NormalTok{)}
|
||
\NormalTok{science\_fiction }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Sciences{-}fiction"}\NormalTok{)}
|
||
\NormalTok{computers }\OperatorTok{=}\NormalTok{ Category.objects.create(name}\OperatorTok{=}\StringTok{"Sciences Informatiques"}\NormalTok{)}
|
||
|
||
\NormalTok{books }\OperatorTok{=}\NormalTok{ \{}
|
||
\StringTok{"Harry Potter"}\NormalTok{: movies,}
|
||
\StringTok{"The Great Gatsby"}\NormalTok{: movies,}
|
||
\StringTok{"Dune"}\NormalTok{: science\_fiction,}
|
||
\StringTok{"H2G2"}\NormalTok{: science\_fiction,}
|
||
\StringTok{"Ender\textquotesingle{}s Game"}\NormalTok{: science\_fiction,}
|
||
\StringTok{"Le seigneur des anneaux"}\NormalTok{: medieval,}
|
||
\StringTok{"L\textquotesingle{}Assassin Royal"}\NormalTok{, medieval,}
|
||
\StringTok{"Clean code"}\NormalTok{: computers,}
|
||
\StringTok{"Designing Data{-}Intensive Applications"}\NormalTok{: computers}
|
||
\NormalTok{\}}
|
||
|
||
\ControlFlowTok{for}\NormalTok{ book\_title, category }\KeywordTok{in}\NormalTok{ books.items:}
|
||
\NormalTok{ Book.objects.create(name}\OperatorTok{=}\NormalTok{book\_title, category}\OperatorTok{=}\NormalTok{category)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Nous nous rendons rapidement compte qu'un livre peut appartenir à
|
||
plusieurs catégories:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\emph{Dune} a été adapté au cinéma en 1973 et en 2021, de même que
|
||
\emph{Le Seigneur des Anneaux}. Ces deux titres (au moins) peuvent
|
||
appartenir à deux catégories distinctes.
|
||
\item
|
||
Pour \emph{The Great Gatsby}, c'est l'inverse: nous l'avons
|
||
initialement classé comme film, mais le livre existe depuis 1925.
|
||
\item
|
||
Nous pourrions sans doute également étoffer notre bibliothèque avec
|
||
une catégorie spéciale "Baguettes magiques et trucs phalliques", à
|
||
laquelle nous pourrons associer la saga \emph{Harry Potter} et ses
|
||
dérivés.
|
||
\end{itemize}
|
||
|
||
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour
|
||
qu'une occurrence puisse être liée à plusieurs catégories. Au lieu
|
||
d'utiliser un champ de type \texttt{ForeignKey}, nous utiliserons un
|
||
champ de type \texttt{ManyToMany}, c'est-à-dire qu'à présent, un livre
|
||
pourra être lié à plusieurs catégories, et qu'inversément, une même
|
||
catégorie pourra être liée à plusieurs livres.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Category(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
\KeywordTok{class}\NormalTok{ Book(models.Model):}
|
||
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ManyManyField(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Notre code d'initialisation reste par contre identique: Django s'occupe
|
||
parfaitement de gérer la transition.
|
||
|
||
\hypertarget{_accuxe8s_aux_relations}{%
|
||
\subsubsection{Accès aux relations}\label{_accuxe8s_aux_relations}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# wish/models.py}
|
||
|
||
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
|
||
\ControlFlowTok{pass}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Item(models.Model):}
|
||
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ models.ForeignKey(Wishlist)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Depuis le code, à partir de l'instance de la classe \texttt{Item}, on
|
||
peut donc accéder à la liste en appelant la propriété \texttt{wishlist}
|
||
de notre instance. \textbf{A contrario}, depuis une instance de type
|
||
\texttt{Wishlist}, on peut accéder à tous les éléments liés grâce à
|
||
\texttt{\textless{}nom\ de\ la\ propriété\textgreater{}\_set}; ici
|
||
\texttt{item\_set}.
|
||
|
||
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes,
|
||
vous pouvez ajouter l'attribut \texttt{related\_name} afin de nommer la
|
||
relation inverse.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# wish/models.py}
|
||
|
||
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
|
||
\ControlFlowTok{pass}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Item(models.Model):}
|
||
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ models.ForeignKey(Wishlist, related\_name}\OperatorTok{=}\StringTok{\textquotesingle{}items\textquotesingle{}}\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Si, dans une classe A, plusieurs relations sont liées à une classe B,
|
||
Django ne saura pas à quoi correspondra la relation inverse. Pour palier
|
||
à ce problème, nous fixons une valeur à l'attribut
|
||
\texttt{related\_name}. Par facilité (et par conventions), prenez
|
||
l'habitude de toujours ajouter cet attribut: votre modèle gagnera en
|
||
cohérence et en lisibilité. Si cette relation inverse n'est pas
|
||
nécessaire, il est possible de l'indiquer (par convention) au travers de
|
||
l'attribut \texttt{related\_name="+"}.
|
||
|
||
A partir de maintenant, nous pouvons accéder à nos propriétés de la
|
||
manière suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# python manage.py shell}
|
||
|
||
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}} \ImportTok{from}\NormalTok{ wish.models }\ImportTok{import}\NormalTok{ Wishlist, Item}
|
||
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist.create(}\StringTok{\textquotesingle{}Liste de test\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{)}
|
||
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ item }\OperatorTok{=}\NormalTok{ Item.create(}\StringTok{\textquotesingle{}Element de test\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{, w)}
|
||
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}
|
||
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ item.wishlist}
|
||
\OperatorTok{\textless{}}\NormalTok{Wishlist: Wishlist }\BuiltInTok{object}\OperatorTok{\textgreater{}}
|
||
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}
|
||
\OperatorTok{\textgreater{}\textgreater{}\textgreater{}}\NormalTok{ wishlist.items.}\BuiltInTok{all}\NormalTok{()}
|
||
\NormalTok{[}\OperatorTok{\textless{}}\NormalTok{Item: Item }\BuiltInTok{object}\OperatorTok{\textgreater{}}\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_n1_queries}{%
|
||
\subsubsection{N+1 Queries}\label{_n1_queries}}
|
||
|
||
\hypertarget{_unicituxe9}{%
|
||
\subsection{Unicité}\label{_unicituxe9}}
|
||
|
||
\hypertarget{_indices}{%
|
||
\subsection{Indices}\label{_indices}}
|
||
|
||
\hypertarget{_conclusions}{%
|
||
\subsubsection{Conclusions}\label{_conclusions}}
|
||
|
||
Dans les examples ci-dessus, nous avons vu les relations multiples
|
||
(1-N), représentées par des clés étrangères (\textbf{ForeignKey}) d'une
|
||
classe A vers une classe B. Pour représenter d'autres types de
|
||
relations, il existe également les champs de type
|
||
\textbf{ManyToManyField}, afin de représenter une relation N-N. Il
|
||
existe également un type de champ spécial pour les clés étrangères, qui
|
||
est le Les champs de type \textbf{OneToOneField}, pour représenter une
|
||
relation 1-1.
|
||
|
||
\hypertarget{_metamoduxe8le_et_introspection}{%
|
||
\subsubsection{Metamodèle et
|
||
introspection}\label{_metamoduxe8le_et_introspection}}
|
||
|
||
Comme chaque classe héritant de \texttt{models.Model} possède une
|
||
propriété \texttt{objects}. Comme on l'a vu dans la section
|
||
\textbf{Jouons un peu avec la console}, cette propriété permet d'accéder
|
||
aux objects persistants dans la base de données, au travers d'un
|
||
\texttt{ModelManager}.
|
||
|
||
En plus de cela, il faut bien tenir compte des propriétés \texttt{Meta}
|
||
de la classe: si elle contient déjà un ordre par défaut, celui-ci sera
|
||
pris en compte pour l'ensemble des requêtes effectuées sur cette classe.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Wish(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ ordering }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}name\textquotesingle{}}\NormalTok{,) }
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Nous définissons un ordre par défaut, directement au niveau du modèle.
|
||
Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre
|
||
(la méthode \texttt{order\_by} existe et peut être chaînée à n'importe
|
||
quel \emph{queryset}). D'où l'intérêt de tester ce type de
|
||
comportement, dans la mesure où un \texttt{top\ 1} dans votre code
|
||
pourrait être modifié simplement par cette petite information.
|
||
\end{itemize}
|
||
|
||
Pour sélectionner un objet au pif :
|
||
\texttt{return\ Category.objects.order\_by("?").first()}
|
||
|
||
Les propriétés de la classe Meta les plus utiles sont les suivates:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\texttt{ordering} pour spécifier un ordre de récupération spécifique.
|
||
\item
|
||
\texttt{verbose\_name} pour indiquer le nom à utiliser au singulier
|
||
pour définir votre classe
|
||
\item
|
||
\texttt{verbose\_name\_plural}, pour le pluriel.
|
||
\item
|
||
\texttt{contraints} (Voir
|
||
\href{https://girlthatlovestocode.com/django-model}{ici}-), par
|
||
exemple
|
||
\end{itemize}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{ constraints }\OperatorTok{=}\NormalTok{ [ }\CommentTok{\# constraints added}
|
||
\NormalTok{ models.CheckConstraint(check}\OperatorTok{=}\NormalTok{models.Q(year\_born\_\_lte}\OperatorTok{=}\NormalTok{datetime.date.today().year}\OperatorTok{{-}}\DecValTok{18}\NormalTok{), name}\OperatorTok{=}\StringTok{\textquotesingle{}will\_be\_of\_age\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{ ]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_choix}{%
|
||
\subsubsection{Choix}\label{_choix}}
|
||
|
||
Voir \href{https://girlthatlovestocode.com/django-model}{ici}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Runner(models.Model):}
|
||
|
||
\CommentTok{\# this is new:}
|
||
\KeywordTok{class}\NormalTok{ Zone(models.IntegerChoices):}
|
||
\NormalTok{ ZONE\_1 }\OperatorTok{=} \DecValTok{1}\NormalTok{, }\StringTok{\textquotesingle{}Less than 3.10\textquotesingle{}}
|
||
\NormalTok{ ZONE\_2 }\OperatorTok{=} \DecValTok{2}\NormalTok{, }\StringTok{\textquotesingle{}Less than 3.25\textquotesingle{}}
|
||
\NormalTok{ ZONE\_3 }\OperatorTok{=} \DecValTok{3}\NormalTok{, }\StringTok{\textquotesingle{}Less than 3.45\textquotesingle{}}
|
||
\NormalTok{ ZONE\_4 }\OperatorTok{=} \DecValTok{4}\NormalTok{, }\StringTok{\textquotesingle{}Less than 4 hours\textquotesingle{}}
|
||
\NormalTok{ ZONE\_5 }\OperatorTok{=} \DecValTok{5}\NormalTok{, }\StringTok{\textquotesingle{}More than 4 hours\textquotesingle{}}
|
||
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)}
|
||
\NormalTok{ last\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)}
|
||
\NormalTok{ email }\OperatorTok{=}\NormalTok{ models.EmailField()}
|
||
\BuiltInTok{id} \OperatorTok{=}\NormalTok{ models.UUIDField(primary\_key}\OperatorTok{=}\VariableTok{True}\NormalTok{, default}\OperatorTok{=}\NormalTok{uuid.uuid4, editable}\OperatorTok{=}\VariableTok{False}\NormalTok{)}
|
||
\NormalTok{ start\_zone }\OperatorTok{=}\NormalTok{ models.PositiveSmallIntegerField(choices}\OperatorTok{=}\NormalTok{Zone.choices, default}\OperatorTok{=}\NormalTok{Zone.ZONE\_5, help\_text}\OperatorTok{=}\StringTok{"What was your best time on the marathon in last 2 years?"}\NormalTok{) }\CommentTok{\# this is new}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_validateurs}{%
|
||
\subsubsection{Validateurs}\label{_validateurs}}
|
||
|
||
\hypertarget{_constructeurs}{%
|
||
\subsubsection{Constructeurs}\label{_constructeurs}}
|
||
|
||
Si vous décidez de définir un constructeur sur votre modèle, ne
|
||
surchargez pas la méthode \texttt{init}: créez plutôt une méthode static
|
||
de type \texttt{create()}, en y associant les paramètres obligatoires ou
|
||
souhaités:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
|
||
|
||
\AttributeTok{@staticmethod}
|
||
\KeywordTok{def}\NormalTok{ create(name, description):}
|
||
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist()}
|
||
\NormalTok{ w.name }\OperatorTok{=}\NormalTok{ name}
|
||
\NormalTok{ w.description }\OperatorTok{=}\NormalTok{ description}
|
||
\NormalTok{ w.save()}
|
||
\ControlFlowTok{return}\NormalTok{ w}
|
||
|
||
\KeywordTok{class}\NormalTok{ Item(models.Model):}
|
||
|
||
\AttributeTok{@staticmethod}
|
||
\KeywordTok{def}\NormalTok{ create(name, description, wishlist):}
|
||
\NormalTok{ i }\OperatorTok{=}\NormalTok{ Item()}
|
||
\NormalTok{ i.name }\OperatorTok{=}\NormalTok{ name}
|
||
\NormalTok{ i.description }\OperatorTok{=}\NormalTok{ description}
|
||
\NormalTok{ i.wishlist }\OperatorTok{=}\NormalTok{ wishlist}
|
||
\NormalTok{ i.save()}
|
||
\ControlFlowTok{return}\NormalTok{ i}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Mieux encore: on pourrait passer par un \texttt{ModelManager} pour
|
||
limiter le couplage; l'accès à une information stockée en base de
|
||
données ne se ferait dès lors qu'au travers de cette instance et pas
|
||
directement au travers du modèle. De cette manière, on limite le
|
||
couplage des classes et on centralise l'accès.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ ItemManager(...):}
|
||
\NormalTok{ (de mémoire, je ne sais plus exactement :}\OperatorTok{{-}}\NormalTok{))}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_conclusion_2}{%
|
||
\subsection{Conclusion}\label{_conclusion_2}}
|
||
|
||
Le modèle proposé par Django est un composant extrêmement performant,
|
||
mais fort couplé avec le coeur du framework. Si tous les composants
|
||
peuvent être échangés avec quelques manipulations, le cas du modèle sera
|
||
plus difficile à interchanger.
|
||
|
||
A côté de cela, il permet énormément de choses, et vous fera gagner un
|
||
temps précieux, tant en rapidité d'essais/erreurs, que de preuves de
|
||
concept.
|
||
|
||
Une possibilité peut également être de cantonner Django à un framework
|
||
|
||
\hypertarget{_querysets_managers}{%
|
||
\subsection{Querysets \& managers}\label{_querysets_managers}}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\url{http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm}
|
||
\item
|
||
\url{https://docs.djangoproject.com/en/1.9/ref/models/querysets/\#django.db.models.query.QuerySet.iterator}
|
||
\item
|
||
\url{http://blog.etianen.com/blog/2013/06/08/django-querysets/}
|
||
\end{itemize}
|
||
|
||
L'ORM de Django (et donc, chacune des classes qui composent votre
|
||
modèle) propose par défaut deux objets hyper importants:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Les \texttt{managers}, qui consistent en un point d'entrée pour
|
||
accéder aux objets persistants
|
||
\item
|
||
Les \texttt{querysets}, qui permettent de filtrer des ensembles ou
|
||
sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour
|
||
ajouter d'autres filtres à des filtres existants, et fonctionnent
|
||
comme un super jeu d'abstraction pour accéder à nos données
|
||
(persistentes).
|
||
\end{itemize}
|
||
|
||
Ces deux propriétés vont de paire; par défaut, chaque classe de votre
|
||
modèle propose un attribut \texttt{objects}, qui correspond à un manager
|
||
(ou un gestionnaire, si vous préférez). Ce gestionnaire constitue
|
||
l'interface par laquelle vous accéderez à la base de données. Mais pour
|
||
cela, vous aurez aussi besoin d'appliquer certains requêtes ou filtres.
|
||
Et pour cela, vous aurez besoin des \texttt{querysets}, qui consistent
|
||
en des \ldots\hspace{0pt} ensembles de requêtes :-).
|
||
|
||
Si on veut connaître la requête SQL sous-jacente à l'exécution du
|
||
queryset, il suffit d'appeler la fonction str() sur la propriété
|
||
\texttt{query}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{queryset }\OperatorTok{=}\NormalTok{ Wishlist.objects.}\BuiltInTok{all}\NormalTok{()}
|
||
|
||
\BuiltInTok{print}\NormalTok{(queryset.query)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Conditions AND et OR sur un queryset
|
||
|
||
\begin{verbatim}
|
||
Pour un `AND`, il suffit de chaîner les conditions. ** trouver un exemple ici ** :-)
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
Mais en gros : bidule.objects.filter(condition1, condition2)
|
||
\end{verbatim}
|
||
|
||
\begin{verbatim}
|
||
Il existe deux autres options : combiner deux querysets avec l'opérateur `&` ou combiner des Q objects avec ce même opérateur.
|
||
\end{verbatim}
|
||
|
||
Soit encore combiner des filtres:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ core.models }\ImportTok{import}\NormalTok{ Wish}
|
||
|
||
\NormalTok{Wish.objects }
|
||
\NormalTok{Wish.objects.}\BuiltInTok{filter}\NormalTok{(name\_\_icontains}\OperatorTok{=}\StringTok{"test"}\NormalTok{).}\BuiltInTok{filter}\NormalTok{(name\_\_icontains}\OperatorTok{=}\StringTok{"too"}\NormalTok{) }
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Ca, c'est notre manager.
|
||
\item
|
||
Et là, on chaîne les requêtes pour composer une recherche sur tous les
|
||
souhaits dont le nom contient (avec une casse insensible) la chaîne
|
||
"test" et dont le nom contient la chaîne "too".
|
||
\end{itemize}
|
||
|
||
Pour un 'OR', on a deux options :
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Soit passer par deux querysets, typiuqment
|
||
\texttt{queryset1\ \textbar{}\ queryset2}
|
||
\item
|
||
Soit passer par des \texttt{Q\ objects}, que l'on trouve dans le
|
||
namespace \texttt{django.db.models}.
|
||
\end{enumerate}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.db.models }\ImportTok{import}\NormalTok{ Q}
|
||
|
||
\NormalTok{condition1 }\OperatorTok{=}\NormalTok{ Q(...)}
|
||
\NormalTok{condition2 }\OperatorTok{=}\NormalTok{ Q(...)}
|
||
|
||
\NormalTok{bidule.objects.}\BuiltInTok{filter}\NormalTok{(condition1 }\OperatorTok{|}\NormalTok{ condition2)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
L'opérateur inverse (\emph{NOT})
|
||
|
||
Idem que ci-dessus : soit on utilise la méthode \texttt{exclude} sur le
|
||
queryset, soit l'opérateur \texttt{\textasciitilde{}} sur un Q object;
|
||
|
||
Ajouter les sujets suivants :
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Prefetch
|
||
\item
|
||
select\_related
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_gestionnaire_de_models_managers_et_opuxe9rations}{%
|
||
\subsubsection{Gestionnaire de models (managers) et
|
||
opérations}\label{_gestionnaire_de_models_managers_et_opuxe9rations}}
|
||
|
||
Chaque définition de modèle utilise un \texttt{Manager}, afin d'accéder
|
||
à la base de données et traiter nos demandes. Indirectement, une
|
||
instance de modèle ne \textbf{connait} \textbf{pas} la base de données:
|
||
c'est son gestionnaire qui a cette tâche. Il existe deux exceptions à
|
||
cette règle: les méthodes \texttt{save()} et \texttt{update()}.
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Instanciation: MyClass()
|
||
\item
|
||
Récupération: MyClass.objects.get(pk=\ldots\hspace{0pt})
|
||
\item
|
||
Sauvegarde : MyClass().save()
|
||
\item
|
||
Création: MyClass.objects.create(\ldots\hspace{0pt})
|
||
\item
|
||
Liste des enregistrements: MyClass.objects.all()
|
||
\end{itemize}
|
||
|
||
Par défaut, le gestionnaire est accessible au travers de la propriété
|
||
\texttt{objects}. Cette propriété a une double utilité:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Elle est facile à surcharger - il nous suffit de définir une nouvelle
|
||
classe héritant de ModelManager, puis de définir, au niveau de la
|
||
classe, une nouvelle assignation à la propriété \texttt{objects}
|
||
\item
|
||
Il est tout aussi facile de définir d'autres propriétés présentant des
|
||
filtres bien spécifiques.
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_requuxeates}{%
|
||
\subsubsection{Requêtes}\label{_requuxeates}}
|
||
|
||
DANGER: Les requêtes sont sensibles à la casse, \textbf{même} si le
|
||
moteur de base de données ne l'est pas. C'est notamment le cas pour
|
||
Microsoft SQL Server; faire une recherche directement via les outils de
|
||
Microsoft ne retournera pas obligatoirement les mêmes résultats que les
|
||
managers, qui seront beaucoup plus tatillons sur la qualité des
|
||
recherches par rapport aux filtres paramétrés en entrée.
|
||
|
||
\hypertarget{_jointures}{%
|
||
\subsubsection{Jointures}\label{_jointures}}
|
||
|
||
Pour appliquer une jointure sur un modèle, nous pouvons passer par les
|
||
méthodes \texttt{select\_related} et \texttt{prefetch\_related}. Il faut
|
||
cependant faire \textbf{très} attention au prefetch related, qui
|
||
fonctionne en fait comme une grosse requête dans laquelle nous trouvons
|
||
un \texttt{IN\ (\ldots{}\hspace{0pt})}. Càd que Django va récupérer tous
|
||
les objets demandés initialement par le queryset, pour ensuite prendre
|
||
toutes les clés primaires, pour finalement faire une deuxième requête et
|
||
récupérer les relations externes.
|
||
|
||
Au final, si votre premier queryset est relativement grand (nous parlons
|
||
de 1000 à 2000 éléments, en fonction du moteur de base de données), la
|
||
seconde requête va planter et vous obtiendrez une exception de type
|
||
\texttt{django.db.utils.OperationalError:\ too\ many\ SQL\ variables}.
|
||
|
||
Nous pourrions penser qu'utiliser un itérateur permettrait de combiner
|
||
les deux, mais ce n'est pas le cas\ldots\hspace{0pt}
|
||
|
||
Comme l'indique la documentation:
|
||
|
||
\begin{verbatim}
|
||
Note that if you use iterator() to run the query, prefetch_related() calls will be ignored since these two optimizations do not make sense together.
|
||
\end{verbatim}
|
||
|
||
Ajouter un itérateur va en fait forcer le code à parcourir chaque
|
||
élément de la liste, pour l'évaluer. Il y aura donc (à nouveau) autant
|
||
de requêtes qu'il y a d'éléments, ce que nous cherchons à éviter.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{informations }\OperatorTok{=}\NormalTok{ (}
|
||
\OperatorTok{\textless{}}\NormalTok{MyObject}\OperatorTok{\textgreater{}}\NormalTok{.objects.}\BuiltInTok{filter}\NormalTok{(}\OperatorTok{\textless{}}\NormalTok{my\_criteria}\OperatorTok{\textgreater{}}\NormalTok{)}
|
||
\NormalTok{ .select\_related(}\OperatorTok{\textless{}}\NormalTok{related\_field}\OperatorTok{\textgreater{}}\NormalTok{)}
|
||
\NormalTok{ .prefetch\_related(}\OperatorTok{\textless{}}\NormalTok{related\_field}\OperatorTok{\textgreater{}}\NormalTok{)}
|
||
\NormalTok{ .iterator(chunk\_size}\OperatorTok{=}\DecValTok{1000}\NormalTok{)}
|
||
\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_aggregate_vs_annotate}{%
|
||
\subsection{Aggregate vs. Annotate}\label{_aggregate_vs_annotate}}
|
||
|
||
\url{https://docs.djangoproject.com/en/3.1/topics/db/aggregation/}
|
||
|
||
\hypertarget{_migrations}{%
|
||
\section{Migrations}\label{_migrations}}
|
||
|
||
Dans cette section, nous allons voir comment fonctionnent les
|
||
migrations. Lors d'une première approche, elles peuvent sembler un peu
|
||
magiques, puisqu'elles centralisent un ensemble de modifications pouvant
|
||
être répétées sur un schéma de données, en tenant compte de ce qui a
|
||
déjà été appliqué et en vérifiant quelles migrations devaient encore
|
||
l'être pour mettre l'application à niveau. Une analyse en profondeur
|
||
montrera qu'elles ne sont pas plus complexes à suivre et à comprendre
|
||
qu'un ensemble de fonctions de gestion appliquées à notre application.
|
||
|
||
La commande \texttt{sqldump}, qui nous présentera le schéma tel qu'il
|
||
sera compris.
|
||
|
||
L'intégration des migrations a été réalisée dans la version 1.7 de
|
||
Django. Avant cela, il convenait de passer par une librairie tierce
|
||
intitulée \href{https://south.readthedocs.io/en/latest}{South}.
|
||
|
||
Prenons l'exemple de notre liste de souhaits; nous nous rendons
|
||
(bêtement) compte que nous avons oublié d'ajouter un champ de
|
||
\texttt{description} à une liste. Historiquement, cette action
|
||
nécessitait l'intervention d'un administrateur système ou d'une personne
|
||
ayant accès au schéma de la base de données, à partir duquel ce-dit
|
||
utilisateur pouvait jouer manuellement un script SQL. sql Cet
|
||
enchaînement d'étapes nécessitait une bonne coordination d'équipe, mais
|
||
également une bonne confiance dans les scripts à exécuter. Et
|
||
souvenez-vous (cf. ref-à-insérer), que l'ensemble des actions doit être
|
||
répétable et automatisable.
|
||
|
||
Bref, dans les années '80, il convenait de jouer ceci après s'être
|
||
connecté au serveur de base de données:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{ALTER} \KeywordTok{TABLE}\NormalTok{ WishList }\KeywordTok{ADD} \KeywordTok{COLUMN}\NormalTok{ Description }\DataTypeTok{nvarchar}\NormalTok{(}\FunctionTok{MAX}\NormalTok{);}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Et là, nous nous rappelons qu'un utilisateur tourne sur Oracle et pas
|
||
sur MySQL, et qu'il a donc besoin de son propre script d'exécution,
|
||
parce que le type du nouveau champ n'est pas exactement le même entre
|
||
les deux moteurs différents:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{{-}{-} Firebird}
|
||
\KeywordTok{ALTER} \KeywordTok{TABLE} \KeywordTok{Category} \KeywordTok{ALTER} \KeywordTok{COLUMN}\NormalTok{ Name }\KeywordTok{type} \DataTypeTok{varchar}\NormalTok{(}\DecValTok{2000}\NormalTok{)}
|
||
|
||
\CommentTok{{-}{-} MSSQL}
|
||
\KeywordTok{ALTER} \KeywordTok{TABLE} \KeywordTok{Category} \KeywordTok{ALTER} \KeywordTok{Column}\NormalTok{ Name }\DataTypeTok{varchar}\NormalTok{(}\DecValTok{2000}\NormalTok{)}
|
||
|
||
\CommentTok{{-}{-} Oracle}
|
||
\KeywordTok{ALTER} \KeywordTok{TABLE} \KeywordTok{Category} \KeywordTok{MODIFY}\NormalTok{ Name }\DataTypeTok{varchar2}\NormalTok{(}\DecValTok{2000}\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
En bref, les problèmes suivants apparaissent très rapidement:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Aucune autonomie: il est nécessaire d'avoir les compétences d'une
|
||
personne tierce pour avancer ou de disposer des droits
|
||
administrateurs,
|
||
\item
|
||
Aucune automatisation possible, à moins d'écrire un programme, qu'il
|
||
faudra également maintenir et intégrer au niveau des tests
|
||
\item
|
||
Nécessité de maintenir autant de scripts différents qu'il y a de
|
||
moteurs de base de données supportés
|
||
\item
|
||
Aucune possibilité de vérifier si le script a déjà été exécuté ou non,
|
||
à moins, à nouveau, de maintenir un programme supplémentaire.
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_fonctionement_guxe9nuxe9ral_2}{%
|
||
\subsection{Fonctionement
|
||
général}\label{_fonctionement_guxe9nuxe9ral_2}}
|
||
|
||
Le moteur de migrations résout la plupart de ces soucis: le framework
|
||
embarque ses propres applications, dont les migrations, qui gèrent
|
||
elles-mêmes l'arbre de dépendances entre les modifications devant être
|
||
appliquées.
|
||
|
||
Pour reprendre un des premiers exemples, nous avions créé un modèle
|
||
contenant deux classes, qui correspondent chacun à une table dans un
|
||
modèle relationnel:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Category(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
\KeywordTok{class}\NormalTok{ Book(models.Model):}
|
||
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ForeignKey(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Nous avions ensuite modifié la clé de liaison, pour permettre d'associer
|
||
plusieurs catégories à un même livre, et inversément:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Category(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
\KeywordTok{class}\NormalTok{ Book(models.Model):}
|
||
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ManyManyField(Category, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Chronologiquement, cela nous a donné une première migration consistant à
|
||
créer le modèle initial, suivie d'une seconde migration après que nous
|
||
ayons modifié le modèle pour autoriser des relations multiples.
|
||
|
||
migrations successives, à appliquer pour que la structure relationnelle
|
||
corresponde aux attentes du modèle Django:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# library/migrations/0001\_initial.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
|
||
\ImportTok{import}\NormalTok{ django.db.models.deletion}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
|
||
|
||
\NormalTok{ initial }\OperatorTok{=} \VariableTok{True}
|
||
|
||
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ []}
|
||
|
||
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ migrations.CreateModel( }
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{"Category"}\NormalTok{,}
|
||
\NormalTok{ fields}\OperatorTok{=}\NormalTok{[}
|
||
\NormalTok{ (}
|
||
\StringTok{"id"}\NormalTok{,}
|
||
\NormalTok{ models.BigAutoField(}
|
||
\NormalTok{ auto\_created}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
|
||
\NormalTok{ primary\_key}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
|
||
\NormalTok{ serialize}\OperatorTok{=}\VariableTok{False}\NormalTok{,}
|
||
\NormalTok{ verbose\_name}\OperatorTok{=}\StringTok{"ID"}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ (}\StringTok{"name"}\NormalTok{, models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)),}
|
||
\NormalTok{ ],}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ migrations.CreateModel( }
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{"Book"}\NormalTok{,}
|
||
\NormalTok{ fields}\OperatorTok{=}\NormalTok{[}
|
||
\NormalTok{ (}
|
||
\StringTok{"id"}\NormalTok{,}
|
||
\NormalTok{ models.BigAutoField(}
|
||
\NormalTok{ auto\_created}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
|
||
\NormalTok{ primary\_key}\OperatorTok{=}\VariableTok{True}\NormalTok{,}
|
||
\NormalTok{ serialize}\OperatorTok{=}\VariableTok{False}\NormalTok{,}
|
||
\NormalTok{ verbose\_name}\OperatorTok{=}\StringTok{"ID"}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ (}
|
||
\StringTok{"title"}\NormalTok{,}
|
||
\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)),}
|
||
\NormalTok{ (}
|
||
\StringTok{"category"}\NormalTok{,}
|
||
\NormalTok{ models.ForeignKey(}
|
||
\NormalTok{ on\_delete}\OperatorTok{=}\NormalTok{django.db.models.deletion.CASCADE,}
|
||
\NormalTok{ to}\OperatorTok{=}\StringTok{"library.category"}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ],}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
La migration crée un nouveau modèle intitulé "Category", possédant un
|
||
champ \texttt{id} (auto-défini, puisque nous n'avons rien fait), ainsi
|
||
qu'un champ \texttt{name} de type texte et d'une longue maximale de
|
||
255 caractères.
|
||
\item
|
||
Elle crée un deuxième modèle intitulé "Book", possédant trois champs:
|
||
son identifiant auto-généré \texttt{id}, son titre \texttt{title} et
|
||
sa relation vers une catégorie, au travers du champ \texttt{category}.
|
||
\end{itemize}
|
||
|
||
Un outil comme \href{https://sqlitebrowser.org/}{DB Browser For SQLite}
|
||
nous donne la structure suivante:
|
||
|
||
\includegraphics{images/db/migrations-0001-to-0002.png}
|
||
|
||
La représentation au niveau de la base de données est la suivante:
|
||
|
||
\includegraphics{images/db/link-book-category-fk.drawio.png}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Category(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
\KeywordTok{class}\NormalTok{ Book(models.Model):}
|
||
\NormalTok{ title }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ category }\OperatorTok{=}\NormalTok{ models.ManyManyField(Category) }
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Vous noterez que l'attribut \texttt{on\_delete} n'est plus nécessaire.
|
||
\end{itemize}
|
||
|
||
Après cette modification, la migration résultante à appliquer
|
||
correspondra à ceci. En SQL, un champ de type \texttt{ManyToMany} ne
|
||
peut qu'être représenté par une table intermédiaire. C'est qu'applique
|
||
la migration en supprimant le champ liant initialement un livre à une
|
||
catégorie et en ajoutant une nouvelle table de liaison.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# library/migrations/0002\_remove\_book\_category\_book\_category.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
|
||
|
||
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ (}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0001\_initial\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{ ]}
|
||
|
||
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ migrations.RemoveField( }
|
||
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ migrations.AddField( }
|
||
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.ManyToManyField(to}\OperatorTok{=}\StringTok{\textquotesingle{}library.Category\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
La migration supprime l'ancienne clé étrangère\ldots\hspace{0pt}
|
||
\item
|
||
\ldots\hspace{0pt} et ajoute une nouvelle table, permettant de lier
|
||
nos catégories à nos livres.
|
||
\end{itemize}
|
||
|
||
\includegraphics{images/db/migrations-0002-many-to-many.png}
|
||
|
||
Nous obtenons à présent la représentation suivante en base de données:
|
||
|
||
\includegraphics{images/db/link-book-category-m2m.drawio.png}
|
||
|
||
\hypertarget{_graph_de_duxe9pendances}{%
|
||
\subsection{Graph de dépendances}\label{_graph_de_duxe9pendances}}
|
||
|
||
Lorsqu'une migration applique une modification au schéma d'une base de
|
||
données, il est évident qu'elle ne peut pas être appliquée dans
|
||
n'importe quel ordre ou à n'importe quel moment.
|
||
|
||
Dès la création d'un nouveau projet, avec une configuration par défaut
|
||
et même sans avoir ajouté d'applications, Django proposera immédiatement
|
||
d'appliquer les migrations des applications \textbf{admin},
|
||
\textbf{auth}, \textbf{contenttypes} et \textbf{sessions}, qui font
|
||
partie du coeur du système, et qui se trouvent respectivement aux
|
||
emplacements suivants:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
\textbf{admin}: \texttt{site-packages/django/contrib/admin/migrations}
|
||
\item
|
||
\textbf{auth}: \texttt{site-packages/django/contrib/auth/migrations}
|
||
\item
|
||
\textbf{contenttypes}:
|
||
\texttt{site-packages/django/contrib/contenttypes/migrations}
|
||
\item
|
||
\textbf{sessions}:
|
||
\texttt{site-packages/django/contrib/sessions/migrations}
|
||
\end{itemize}
|
||
|
||
Ceci est dû au fait que, toujours par défaut, ces applications sont
|
||
reprises au niveau de la configuration d'un nouveau projet, dans le
|
||
fichier \texttt{settings.py}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{[snip]}
|
||
|
||
\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [}
|
||
\StringTok{\textquotesingle{}django.contrib.admin\textquotesingle{}}\NormalTok{, }
|
||
\StringTok{\textquotesingle{}django.contrib.auth\textquotesingle{}}\NormalTok{, }
|
||
\StringTok{\textquotesingle{}django.contrib.contenttypes\textquotesingle{}}\NormalTok{, }
|
||
\StringTok{\textquotesingle{}django.contrib.sessions\textquotesingle{}}\NormalTok{, }
|
||
\StringTok{\textquotesingle{}django.contrib.messages\textquotesingle{}}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}django.contrib.staticfiles\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{]}
|
||
|
||
\NormalTok{[snip]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Admin
|
||
\item
|
||
Auth
|
||
\item
|
||
Contenttypes
|
||
\item
|
||
et Sessions.
|
||
\end{itemize}
|
||
|
||
Dès que nous les appliquerons, nous recevrons les messages suivants:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py migrate}
|
||
\ExtensionTok{Operations}\NormalTok{ to perform:}
|
||
\ExtensionTok{Apply}\NormalTok{ all migrations: admin, auth, contenttypes, library, sessions, world}
|
||
\ExtensionTok{Running}\NormalTok{ migrations:}
|
||
\ExtensionTok{Applying}\NormalTok{ contenttypes.0001\_initial... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0001\_initial... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ admin.0001\_initial... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ admin.0002\_logentry\_remove\_auto\_add... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ admin.0003\_logentry\_add\_action\_flag\_choices... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ contenttypes.0002\_remove\_content\_type\_name... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0002\_alter\_permission\_name\_max\_length... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0003\_alter\_user\_email\_max\_length... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0004\_alter\_user\_username\_opts... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0005\_alter\_user\_last\_login\_null... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0006\_require\_contenttypes\_0002... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0007\_alter\_validators\_add\_error\_messages... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0008\_alter\_user\_username\_max\_length... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0009\_alter\_user\_last\_name\_max\_length... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0010\_alter\_group\_name\_max\_length... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0011\_update\_proxy\_permissions... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ auth.0012\_alter\_user\_first\_name\_max\_length... OK}
|
||
\ExtensionTok{Applying}\NormalTok{ sessions.0001\_initial... OK}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Cet ordre est défini au niveau de la propriété \texttt{dependencies},
|
||
que l'on retrouve au niveau de chaque description de migration, En
|
||
explorant les paquets qui se trouvent au niveau des répertoires et en
|
||
analysant les dépendances décrites au niveau de chaque action de
|
||
migration, on arrive au schéma suivant, qui est un graph dirigé
|
||
acyclique:
|
||
|
||
\includegraphics{images/db/migrations_auth_admin_contenttypes_sessions.png}
|
||
|
||
\hypertarget{_sous_le_capot}{%
|
||
\subsection{Sous le capot}\label{_sous_le_capot}}
|
||
|
||
Une migration consiste à appliquer un ensemble de modifications (ou
|
||
\textbf{opérations}), qui exercent un ensemble de transformations, pour
|
||
que le schéma de base de données corresponde au modèle de l'application
|
||
sous-jacente.
|
||
|
||
Les migrations (comprendre les "\emph{migrations du schéma de base de
|
||
données}") sont intimement liées à la représentation d'un contexte
|
||
fonctionnel: l'ajout d'une nouvelle information, d'un nouveau champ ou
|
||
d'une nouvelle fonction peut s'accompagner de tables de données à mettre
|
||
à jour ou de champs à étendre. Il est primordial que la structure de la
|
||
base de données corresponde à ce à quoi l'application s'attend, sans
|
||
quoi la probabilité que l'utilisateur tombe sur une erreur de type
|
||
\texttt{django.db.utils.OperationalError} est (très) grande.
|
||
Typiquement, après avoir ajouté un nouveau champ \texttt{summary} à
|
||
chacun de nos livres, et sans avoir appliqué de migrations, nous tombons
|
||
sur ceci:
|
||
|
||
\begin{verbatim}
|
||
>>> from library.models import Book
|
||
>>> Book.objects.all()
|
||
Traceback (most recent call last):
|
||
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/utils.py", line 85, in _execute
|
||
return self.cursor.execute(sql, params)
|
||
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/sqlite3/base.py", line 416, in execute
|
||
return Database.Cursor.execute(self, query, params)
|
||
sqlite3.OperationalError: no such column: library_book.summary
|
||
\end{verbatim}
|
||
|
||
Pour éviter ce type d'erreurs, il est impératif que les nouvelles
|
||
migrations soient appliquées \textbf{avant} que le code ne soit déployé;
|
||
l'idéal étant que ces deux opérations soient réalisées de manière
|
||
atomique, avec un \emph{rollback} si une anomalie était détectée.
|
||
|
||
En allant
|
||
|
||
Pour éviter ce type d'erreurs, plusieurs stratégies peuvent être
|
||
appliquées:
|
||
|
||
intégrer ici un point sur les updates db - voir designing data-intensive
|
||
applications.
|
||
|
||
Toujours dans une optique de centralisation, les migrations sont
|
||
directement embarquées au niveau du code, et doivent faire partie du
|
||
dépôt central de sources. Le développeur s'occupe de créer les
|
||
migrations en fonction des actions à entreprendre; ces migrations
|
||
peuvent être retravaillées, \emph{squashées}, \ldots\hspace{0pt} et
|
||
feront partie intégrante du processus de mise à jour de l'application.
|
||
|
||
A noter que les migrations n'appliqueront de modifications que si le
|
||
schéma est impacté. Ajouter une propriété \texttt{related\_name} sur une
|
||
ForeignKey n'engendrera aucune nouvelle action de migration, puisque ce
|
||
type d'action ne s'applique que sur l'ORM, et pas directement sur la
|
||
base de données: au niveau des tables, rien ne change. Seul le code et
|
||
le modèle sont impactés.
|
||
|
||
Une migration est donc une classe Python, présentant \emph{a minima}
|
||
deux propriétés:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
\texttt{dependencies}, qui décrit les opérations précédentes devant
|
||
obligatoirement avoir été appliquées
|
||
\item
|
||
\texttt{operations}, qui consiste à décrire précisément ce qui doit
|
||
être exécuté.
|
||
\end{enumerate}
|
||
|
||
Pour reprendre notre exemple d'ajout d'un champ \texttt{description} sur
|
||
le modèle \texttt{WishList}, la migration ressemblera à ceci:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
|
||
\ImportTok{import}\NormalTok{ django.db.models.deletion}
|
||
\ImportTok{import}\NormalTok{ django.utils.timezone}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
|
||
|
||
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ (}\StringTok{\textquotesingle{}gwift\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0004\_name\_value\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{ ]}
|
||
|
||
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ migrations.AddField(}
|
||
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlist\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.TextField(default}\OperatorTok{=}\StringTok{""}\NormalTok{, null}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
|
||
\NormalTok{ preserve\_default}\OperatorTok{=}\VariableTok{False}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_liste_des_migrations}{%
|
||
\subsection{Liste des migrations}\label{_liste_des_migrations}}
|
||
|
||
L'option \texttt{showmigrations} de \texttt{manage.py} permet de lister
|
||
toutes les migrations du projet, et d'identifier celles qui n'auraient
|
||
pas encore été appliquées:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py showmigrations}
|
||
\ExtensionTok{admin}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_logentry\_remove\_auto\_add}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0003\_logentry\_add\_action\_flag\_choices}
|
||
\ExtensionTok{auth}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_alter\_permission\_name\_max\_length}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0003\_alter\_user\_email\_max\_length}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0004\_alter\_user\_username\_opts}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0005\_alter\_user\_last\_login\_null}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0006\_require\_contenttypes\_0002}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0007\_alter\_validators\_add\_error\_messages}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0008\_alter\_user\_username\_max\_length}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0009\_alter\_user\_last\_name\_max\_length}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0010\_alter\_group\_name\_max\_length}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0011\_update\_proxy\_permissions}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0012\_alter\_user\_first\_name\_max\_length}
|
||
\ExtensionTok{contenttypes}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_remove\_content\_type\_name}
|
||
\ExtensionTok{library}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0002\_remove\_book\_category\_book\_category}
|
||
\BuiltInTok{ [ ]} \ExtensionTok{0003\_book\_summary}
|
||
\ExtensionTok{sessions}
|
||
\NormalTok{ [}\ExtensionTok{X}\NormalTok{] 0001\_initial}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_squash}{%
|
||
\subsection{Squash}\label{_squash}}
|
||
|
||
Finalement, lorsque vous développez sur votre propre branche (cf.
|
||
\protect\hyperlink{git}{???}), vous serez peut-être tentés de créer
|
||
plusieurs migrations en fonction de l'évolution de ce que vous mettez en
|
||
place. Dans ce cas précis, il peut être intéressant d'utiliser la
|
||
méthode \texttt{squashmigrations}, qui permet \emph{d'aplatir} plusieurs
|
||
fichiers en un seul.
|
||
|
||
Nous partons dans deux migrations suivantes:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# library/migrations/0002\_remove\_book\_category.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
|
||
|
||
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ (}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0001\_initial\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{ ]}
|
||
|
||
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ migrations.RemoveField(}
|
||
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ migrations.AddField(}
|
||
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.ManyToManyField(to}\OperatorTok{=}\StringTok{\textquotesingle{}library.Category\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# library/migrations/0003\_book\_summary.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ migrations, models}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Migration(migrations.Migration):}
|
||
|
||
\NormalTok{ dependencies }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ (}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0002\_remove\_book\_category\_book\_category\textquotesingle{}}\NormalTok{),}
|
||
\NormalTok{ ]}
|
||
|
||
\NormalTok{ operations }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ migrations.AddField(}
|
||
\NormalTok{ model\_name}\OperatorTok{=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}summary\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ field}\OperatorTok{=}\NormalTok{models.TextField(blank}\OperatorTok{=}\VariableTok{True}\NormalTok{),}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
La commande
|
||
\texttt{python\ manage.py\ squashmigrations\ library\ 0002\ 0003}
|
||
appliquera une fusion entre les migrations numérotées \texttt{0002} et
|
||
\texttt{0003}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\ExtensionTok{python}\NormalTok{ manage.py squashmigrations library 0002 0003}
|
||
\ExtensionTok{Will}\NormalTok{ squash the following migrations:}
|
||
\ExtensionTok{{-}}\NormalTok{ 0002\_remove\_book\_category\_book\_category}
|
||
\ExtensionTok{{-}}\NormalTok{ 0003\_book\_summary}
|
||
\ExtensionTok{Do}\NormalTok{ you wish to proceed? [yN] y}
|
||
\ExtensionTok{Optimizing...}
|
||
\ExtensionTok{No}\NormalTok{ optimizations possible.}
|
||
\ExtensionTok{Created}\NormalTok{ new squashed migration /home/fred/Sources/gwlib/library/migrations/0002\_remove\_book\_category\_book\_category\_squashed\_0003\_book\_summary.py}
|
||
\ExtensionTok{You}\NormalTok{ should commit this migration but leave the old ones in place}\KeywordTok{;}
|
||
\ExtensionTok{the}\NormalTok{ new migration will be used for new installs. Once you are sure}
|
||
\ExtensionTok{all}\NormalTok{ instances of the codebase have applied the migrations you squashed,}
|
||
\ExtensionTok{you}\NormalTok{ can delete them.}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Dans le cas où vous développez proprement (bis), il est sauf de purement
|
||
et simplement supprimer les anciens fichiers; dans le cas où il pourrait
|
||
exister au moins une instance ayant appliqué ces migrations, les anciens
|
||
\textbf{ne peuvent surtout pas être modifiés}.
|
||
|
||
Nous avons à présent un nouveau fichier intitulé
|
||
\texttt{0002\_remove\_book\_category\_book\_category\_squashed\_0003\_book\_summary}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{$ }\FunctionTok{cat}\NormalTok{ library/migrations/0002\_remove\_book\_category\_book\_category\_squashed\_0003\_book\_summary.py}
|
||
\CommentTok{\# Generated by Django 4.0.3 on 2022{-}03{-}15 18:01}
|
||
|
||
\ExtensionTok{from}\NormalTok{ django.db import migrations, models}
|
||
|
||
|
||
\ExtensionTok{class}\NormalTok{ Migration(migrations.Migration)}\BuiltInTok{:}
|
||
|
||
\ExtensionTok{replaces}\NormalTok{ = [(}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0002\_remove\_book\_category\_book\_category\textquotesingle{}}\NormalTok{), }\KeywordTok{(}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0003\_book\_summary\textquotesingle{}}\KeywordTok{)}\NormalTok{]}
|
||
|
||
\ExtensionTok{dependencies}\NormalTok{ = [}
|
||
\KeywordTok{(}\StringTok{\textquotesingle{}library\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}0001\_initial\textquotesingle{}}\KeywordTok{)}\NormalTok{,}
|
||
\NormalTok{ ]}
|
||
|
||
\ExtensionTok{operations}\NormalTok{ = [}
|
||
\ExtensionTok{migrations.RemoveField}\NormalTok{(}
|
||
\VariableTok{model\_name=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\VariableTok{name=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\ExtensionTok{migrations.AddField}\NormalTok{(}
|
||
\VariableTok{model\_name=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\VariableTok{name=}\StringTok{\textquotesingle{}category\textquotesingle{}}\NormalTok{,}
|
||
\VariableTok{field=}\NormalTok{models.ManyToManyField}\VariableTok{(}\NormalTok{to}\VariableTok{=}\StringTok{\textquotesingle{}library.category\textquotesingle{}}\VariableTok{)}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\ExtensionTok{migrations.AddField}\NormalTok{(}
|
||
\VariableTok{model\_name=}\StringTok{\textquotesingle{}book\textquotesingle{}}\NormalTok{,}
|
||
\VariableTok{name=}\StringTok{\textquotesingle{}summary\textquotesingle{}}\NormalTok{,}
|
||
\VariableTok{field=}\NormalTok{models.TextField}\VariableTok{(}\NormalTok{blank}\VariableTok{=}\NormalTok{True}\VariableTok{)}\NormalTok{,}
|
||
\NormalTok{ ),}
|
||
\NormalTok{ ]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_ruxe9initialisation_dune_ou_plusieurs_migrations}{%
|
||
\subsection{Réinitialisation d'une ou plusieurs
|
||
migrations}\label{_ruxe9initialisation_dune_ou_plusieurs_migrations}}
|
||
|
||
\href{https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html}{reset
|
||
migrations}.
|
||
|
||
\begin{quote}
|
||
\begin{verbatim}
|
||
En gros, soit on supprime toutes les migrations (en conservant le fichier __init__.py), soit on réinitialise proprement les migrations avec un --fake-initial (sous réserve que toutes les personnes qui utilisent déjà le projet s'y conforment... Ce qui n'est pas gagné.
|
||
Pour repartir de notre exemple ci-dessus, nous avions un modèle reprenant quelques classes, saupoudrées de propriétés décrivant nos différents champs. Pour être prise en compte par le moteur de base de données, chaque modification doit être
|
||
\end{verbatim}
|
||
\end{quote}
|
||
|
||
\hypertarget{_shell}{%
|
||
\section{Shell}\label{_shell}}
|
||
|
||
\hypertarget{_administration}{%
|
||
\section{Administration}\label{_administration}}
|
||
|
||
Woké. On va commencer par la \textbf{partie à ne \emph{surtout}
|
||
(\emph{surtout} !!) pas faire en premier dans un projet Django}. Mais on
|
||
va la faire quand même: la raison principale est que cette partie est
|
||
tellement puissante et performante, qu'elle pourrait laisser penser
|
||
qu'il est possible de réaliser une application complète rien qu'en
|
||
configurant l'administration. Mais c'est faux.
|
||
|
||
L'administration est une sorte de tour de contrôle évoluée, un
|
||
\emph{back office} sans transpirer; elle se base sur le modèle de
|
||
données programmé et construit dynamiquement les formulaires qui lui est
|
||
associé. Elle joue avec les clés primaires, étrangères, les champs et
|
||
types de champs par
|
||
\href{https://fr.wikipedia.org/wiki/Introspection}{introspection}, et
|
||
présente tout ce qu'il faut pour avoir du
|
||
\href{https://fr.wikipedia.org/wiki/CRUD}{CRUD}, c'est-à-dire tout ce
|
||
qu'il faut pour ajouter, lister, modifier ou supprimer des informations.
|
||
|
||
Son problème est qu'elle présente une courbe d'apprentissage
|
||
asymptotique. Il est \textbf{très} facile d'arriver rapidement à un bon
|
||
résultat, au travers d'un périmètre de configuration relativement
|
||
restreint. Mais quoi que vous fassiez, il y a un moment où la courbe de
|
||
paramétrage sera tellement ardue que vous aurez plus facile à développer
|
||
ce que vous souhaitez ajouter en utilisant les autres concepts de
|
||
Django.
|
||
|
||
Cette fonctionnalité doit rester dans les mains d'administrateurs ou de
|
||
gestionnaires, et dans leurs mains à eux uniquement: il n'est pas
|
||
question de donner des droits aux utilisateurs finaux (même si c'est
|
||
extrêment tentant durant les premiers tours de roues). Indépendamment de
|
||
la manière dont vous allez l'utiliser et la configurer, vous finirez par
|
||
devoir développer une "vraie" application, destinée aux utilisateurs
|
||
classiques, et répondant à leurs besoins uniquement.
|
||
|
||
Une bonne idée consiste à développer l'administration dans un premier
|
||
temps, en \textbf{gardant en tête qu'il sera nécessaire de développer
|
||
des concepts spécifiques}. Dans cet objectif, l'administration est un
|
||
outil exceptionel, qui permet de valider un modèle, de créer des objets
|
||
rapidement et de valider les liens qui existent entre eux.
|
||
|
||
C'est aussi un excellent outil de prototypage et de preuve de concept.
|
||
|
||
Elle se base sur plusieurs couches que l'on a déjà (ou on va bientôt)
|
||
aborder (suivant le sens de lecture que vous préférez):
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Le modèle de données
|
||
\item
|
||
Les validateurs
|
||
\item
|
||
Les formulaires
|
||
\item
|
||
Les widgets
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_le_moduxe8le_de_donnuxe9es}{%
|
||
\subsection{Le modèle de données}\label{_le_moduxe8le_de_donnuxe9es}}
|
||
|
||
Comme expliqué ci-dessus, le modèle de données est constité d'un
|
||
ensemble de champs typés et de relations. L'administration permet de
|
||
décrire les données qui peuvent être modifiées, en y associant un
|
||
ensemble (basique) de permissions.
|
||
|
||
Si vous vous rappelez de l'application que nous avions créée dans la
|
||
première partie, les URLs reprenaient déjà la partie suivante:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
|
||
\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path}
|
||
|
||
\ImportTok{from}\NormalTok{ gwift.views }\ImportTok{import}\NormalTok{ wish\_details}
|
||
|
||
\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [}
|
||
\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls), }
|
||
\NormalTok{ [...]}
|
||
\NormalTok{]}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Cette URL signifie que la partie \texttt{admin} est déjà active et
|
||
accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}
|
||
\end{itemize}
|
||
|
||
C'est le seul prérequis pour cette partie.
|
||
|
||
Chaque application nouvellement créée contient par défaut un fichier
|
||
\texttt{admin.py}, dans lequel il est possible de déclarer quel ensemble
|
||
de données sera accessible/éditable. Ainsi, si nous partons du modèle
|
||
basique que nous avions détaillé plus tôt, avec des souhaits et des
|
||
listes de souhaits:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\CommentTok{\# gwift/wish/models.py}
|
||
|
||
\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ models}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ WishList(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Item(models.Model):}
|
||
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
|
||
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ models.ForeignKey(WishList, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Nous pouvons facilement arriver au résultat suivant, en ajoutant
|
||
quelques lignes de configuration dans ce fichier \texttt{admin.py}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin}
|
||
|
||
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ Item, WishList }
|
||
|
||
|
||
\NormalTok{admin.site.register(Item) }
|
||
\NormalTok{admin.site.register(WishList)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Nous importons les modèles que nous souhaitons gérer dans l'admin
|
||
\item
|
||
Et nous les déclarons comme gérables. Cette dernière ligne implique
|
||
aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui
|
||
n'activera simplement aucune opération de lecture ou modification.
|
||
\end{itemize}
|
||
|
||
Il nous reste une seule étape à réaliser: créer un nouvel utilisateur.
|
||
Pour cet exemple, notre gestion va se limiter à une gestion manuelle;
|
||
nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons
|
||
créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\NormalTok{λ }\ExtensionTok{python}\NormalTok{ manage.py createsuperuser}
|
||
\ExtensionTok{Username}\NormalTok{ (leave blank to use }\StringTok{\textquotesingle{}fred\textquotesingle{}}\NormalTok{)}\BuiltInTok{:}\NormalTok{ fred}
|
||
\ExtensionTok{Email}\NormalTok{ address: fred@root.org}
|
||
\ExtensionTok{Password}\NormalTok{: ******}
|
||
\ExtensionTok{Password}\NormalTok{ (again)}\BuiltInTok{:}\NormalTok{ ******}
|
||
\ExtensionTok{Superuser}\NormalTok{ created successfully.}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/django-site-admin.png}
|
||
\caption{Connexion au site d'administration}
|
||
\end{figure}
|
||
|
||
\begin{figure}
|
||
\centering
|
||
\includegraphics{images/django/django-site-admin-after-connection.png}
|
||
\caption{Administration}
|
||
\end{figure}
|
||
|
||
\hypertarget{_quelques_conseils_de_base}{%
|
||
\subsection{Quelques conseils de
|
||
base}\label{_quelques_conseils_de_base}}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Surchargez la méthode \texttt{str(self)} pour chaque classe que vous
|
||
aurez définie dans le modèle. Cela permettra de construire une
|
||
représentation textuelle pour chaque instance de votre classe. Cette
|
||
information sera utilisée un peu partout dans le code, et donnera une
|
||
meilleure idée de ce que l'on manipule. En plus, cette méthode est
|
||
également appelée lorsque l'administration historisera une action (et
|
||
comme cette étape sera inaltérable, autant qu'elle soit fixée dans le
|
||
début).
|
||
\item
|
||
La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle
|
||
on peut accéder pour obtenir les détails d'une instance. Par exemple:
|
||
\end{enumerate}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{def}\NormalTok{ get\_absolute\_url(}\VariableTok{self}\NormalTok{):}
|
||
\ControlFlowTok{return}\NormalTok{ reverse(}\StringTok{\textquotesingle{}myapp.views.details\textquotesingle{}}\NormalTok{, args}\OperatorTok{=}\NormalTok{[}\VariableTok{self}\NormalTok{.}\BuiltInTok{id}\NormalTok{])}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Les attributs \texttt{Meta}:
|
||
\end{enumerate}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ ordering }\OperatorTok{=}\NormalTok{ [}\StringTok{\textquotesingle{}{-}field1\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}field2\textquotesingle{}}\NormalTok{]}
|
||
\NormalTok{ verbose\_name }\OperatorTok{=} \StringTok{\textquotesingle{}my class in singular\textquotesingle{}}
|
||
\NormalTok{ verbose\_name\_plural }\OperatorTok{=} \StringTok{\textquotesingle{}my class when is in a list!\textquotesingle{}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Le titre:
|
||
|
||
\begin{itemize}
|
||
\item
|
||
Soit en modifiant le template de l'administration
|
||
\item
|
||
Soit en ajoutant l'assignation suivante dans le fichier
|
||
\texttt{urls.py}:
|
||
\texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}.
|
||
\end{itemize}
|
||
\item
|
||
Prefetch
|
||
\end{enumerate}
|
||
|
||
\url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64}
|
||
|
||
\url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614}
|
||
|
||
En gros, le problème de l'admin est que si on fait des requêtes
|
||
imbriquées, on va flinguer l'application et le chargement de la page. La
|
||
solution consiste à utiliser la propriété \texttt{list\_select\_related}
|
||
de la classe d'Admin, afin d'appliquer une jointure par défaut et et
|
||
gagner en performances.
|
||
|
||
\hypertarget{_admin_modeladmin}{%
|
||
\subsection{admin.ModelAdmin}\label{_admin_modeladmin}}
|
||
|
||
La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement
|
||
dans le fichier \texttt{admin.py} de chaque application contiendra la
|
||
définition de ce que l'on souhaite faire avec nos données dans
|
||
l'administration. Cette classe (et sa partie Meta)
|
||
|
||
\hypertarget{_laffichage}{%
|
||
\subsection{L'affichage}\label{_laffichage}}
|
||
|
||
Comme l'interface d'administration fonctionne (en trèèèès) gros comme un
|
||
CRUD auto-généré, on trouve par défaut la possibilité de :
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Créer de nouveaux éléments
|
||
\item
|
||
Lister les éléments existants
|
||
\item
|
||
Modifier des éléments existants
|
||
\item
|
||
Supprimer un élément en particulier.
|
||
\end{enumerate}
|
||
|
||
Les affichages sont donc de deux types: en liste et par élément.
|
||
|
||
Pour les affichages en liste, le plus simple consiste à jouer sur la
|
||
propriété \texttt{list\_display}.
|
||
|
||
Par défaut, la première colonne va accueillir le lien vers le formulaire
|
||
d'édition. On peut donc modifier ceci, voire créer de nouveaux liens
|
||
vers d'autres éléments en construisant des URLs dynamiquement.
|
||
|
||
(Insérer ici l'exemple de Medplan pour les liens vers les postgradués
|
||
:-))
|
||
|
||
Voir aussi comment personnaliser le fil d'Ariane ?
|
||
|
||
\hypertarget{_les_filtres}{%
|
||
\subsection{Les filtres}\label{_les_filtres}}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
list\_filter
|
||
\item
|
||
filter\_horizontal
|
||
\item
|
||
filter\_vertical
|
||
\item
|
||
date\_hierarchy
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_les_permissions}{%
|
||
\subsection{Les permissions}\label{_les_permissions}}
|
||
|
||
On l'a dit plus haut, il vaut mieux éviter de proposer un accès à
|
||
l'administration à vos utilisateurs. Il est cependant possible de
|
||
configurer des permissions spécifiques pour certains groupes, en leur
|
||
autorisant certaines actions de visualisation/ajout/édition ou
|
||
suppression.
|
||
|
||
Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les
|
||
méthodes suivantes:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{def}\NormalTok{ has\_add\_permission(}\VariableTok{self}\NormalTok{, request):}
|
||
\ControlFlowTok{return} \VariableTok{True}
|
||
|
||
\KeywordTok{def}\NormalTok{ has\_delete\_permission(}\VariableTok{self}\NormalTok{, request):}
|
||
\ControlFlowTok{return} \VariableTok{True}
|
||
|
||
\KeywordTok{def}\NormalTok{ has\_change\_permission(}\VariableTok{self}\NormalTok{, request):}
|
||
\ControlFlowTok{return} \VariableTok{True}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
On peut accéder aux informations de l'utilisateur actuellement connecté
|
||
au travers de l'objet \texttt{request.user}.
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\alph{enumi}.}
|
||
\item
|
||
NOTE: ajouter un ou deux screenshots :-)
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_les_relations}{%
|
||
\subsection{Les relations}\label{_les_relations}}
|
||
|
||
\hypertarget{_les_relations_1_n}{%
|
||
\subsubsection{Les relations 1-n}\label{_les_relations_1_n}}
|
||
|
||
Les relations 1-n sont implémentées au travers de formsets (que l'on a
|
||
normalement déjà décrits plus haut). L'administration permet de les
|
||
définir d'une manière extrêmement simple, grâce à quelques propriétés.
|
||
|
||
L'implémentation consiste tout d'abord à définir le comportement du type
|
||
d'objet référencé (la relation -N), puis à inclure cette définition au
|
||
niveau du type d'objet référençant (la relation 1-).
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{class}\NormalTok{ WishInline(TabularInline):}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Wish}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ Wishlist(admin.ModelAdmin):}
|
||
\NormalTok{ ...}
|
||
\NormalTok{ inlines }\OperatorTok{=}\NormalTok{ [WishInline]}
|
||
\NormalTok{ ...}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist})
|
||
pourra directement gérer des relations multiples vers des souhaits.
|
||
|
||
\hypertarget{_les_auto_suggestions_et_auto_compluxe9tions}{%
|
||
\subsubsection{Les auto-suggestions et
|
||
auto-complétions}\label{_les_auto_suggestions_et_auto_compluxe9tions}}
|
||
|
||
Parler de l'intégration de select2.
|
||
|
||
\hypertarget{_la_pruxe9sentation}{%
|
||
\subsection{La présentation}\label{_la_pruxe9sentation}}
|
||
|
||
Parler ici des \texttt{fieldsets} et montrer comment on peut regrouper
|
||
des champs dans des groupes, ajouter un peu de javascript,
|
||
\ldots\hspace{0pt}
|
||
|
||
\hypertarget{_les_actions_sur_des_suxe9lections}{%
|
||
\subsection{Les actions sur des
|
||
sélections}\label{_les_actions_sur_des_suxe9lections}}
|
||
|
||
Les actions permettent de partir d'une liste d'éléments, et autorisent
|
||
un utilisateur à appliquer une action sur une sélection d'éléments. Par
|
||
défaut, il existe déjà une action de \textbf{suppression}.
|
||
|
||
Les paramètres d'entrée sont :
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
L'instance de classe
|
||
\item
|
||
La requête entrante
|
||
\item
|
||
Le queryset correspondant à la sélection.
|
||
\end{enumerate}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\KeywordTok{def}\NormalTok{ double\_quantity(}\VariableTok{self}\NormalTok{, request, queryset):}
|
||
\ControlFlowTok{for}\NormalTok{ obj }\KeywordTok{in}\NormalTok{ queryset.}\BuiltInTok{all}\NormalTok{():}
|
||
\NormalTok{ obj.field }\OperatorTok{+=} \DecValTok{1}
|
||
\NormalTok{ obj.save()}
|
||
\NormalTok{double\_quantity.short\_description }\OperatorTok{=} \StringTok{"Doubler la quantité des souhaits."}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
Et pour informer l'utilisateur de ce qui a été réalisé, on peut aussi
|
||
lui passer un petit message:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ControlFlowTok{if}\NormalTok{ rows\_updated }\OperatorTok{=} \DecValTok{0}\NormalTok{:}
|
||
\VariableTok{self}\NormalTok{.message\_user(request, }\StringTok{"Aucun élément n\textquotesingle{}a été impacté."}\NormalTok{)}
|
||
\ControlFlowTok{else}\NormalTok{:}
|
||
\VariableTok{self}\NormalTok{.message\_user(request, }\StringTok{"}\SpecialCharTok{\{\}}\StringTok{ élément(s) mis à jour"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(rows\_updated))}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_la_documentation}{%
|
||
\subsection{La documentation}\label{_la_documentation}}
|
||
|
||
Nous l'avons dit plus haut, l'administration de Django a également la
|
||
possibilité de rendre accessible la documentation associée à un modèle
|
||
de données. Pour cela, il suffit de suivre les bonnes pratiques, puis
|
||
\href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer
|
||
la documentation à partir des URLs}:
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_forms}{%
|
||
\section{Forms}\label{_forms}}
|
||
|
||
\begin{quote}
|
||
Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et
|
||
que l'ensemble reste cohérent. Il (le form) n'a pas à savoir que tu as
|
||
implémenté des closure tables dans un graph dirigé acyclique.
|
||
\end{quote}
|
||
|
||
Ou comment valider proprement des données entrantes.
|
||
|
||
\includegraphics{images/xkcd-327.png}
|
||
|
||
Quand on parle de \texttt{forms}, on ne parle pas uniquement de
|
||
formulaires Web. On pourrait considérer qu'il s'agit de leur objectif
|
||
principal, mais on peut également voir un peu plus loin: on peut en fait
|
||
voir les \texttt{forms} comme le point d'entrée pour chaque donnée
|
||
arrivant dans notre application: il s'agit en quelque sorte d'un
|
||
ensemble de règles complémentaires à celles déjà présentes au niveau du
|
||
modèle.
|
||
|
||
L'exemple le plus simple est un fichier \texttt{.csv}: la lecture de ce
|
||
fichier pourrait se faire de manière très simple, en récupérant les
|
||
valeurs de chaque colonne et en l'introduisant dans une instance du
|
||
modèle.
|
||
|
||
Mauvaise idée. On peut proposer trois versions d'un même code, de la
|
||
version simple (lecture du fichier csv et jonglage avec les indices de
|
||
colonnes), puis une version plus sophistiquée (et plus lisible, à base
|
||
de
|
||
\href{https://docs.python.org/3/library/csv.html\#csv.DictReader}{DictReader}),
|
||
et la version + à base de form.
|
||
|
||
Les données fournies par un utilisateur \textbf{doivent}
|
||
\textbf{toujours} être validées avant introduction dans la base de
|
||
données. Notre base de données étant accessible ici par l'ORM, la
|
||
solution consiste à introduire une couche supplémentaire de validation.
|
||
|
||
Le flux à suivre est le suivant:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Création d'une instance grâce à un dictionnaire
|
||
\item
|
||
Validation des données et des informations reçues
|
||
\item
|
||
Traitement, si la validation a réussi.
|
||
\end{enumerate}
|
||
|
||
Ils jouent également deux rôles importants:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Valider des données, en plus de celles déjà définies au niveau du
|
||
modèle
|
||
\item
|
||
Contrôler le rendu à appliquer aux champs.
|
||
\end{enumerate}
|
||
|
||
Ils agissent come une glue entre l'utilisateur et la modélisation de vos
|
||
structures de données.
|
||
|
||
\hypertarget{_flux_de_validation}{%
|
||
\subsection{Flux de validation}\label{_flux_de_validation}}
|
||
|
||
\textbar{} .Validation \textbar{} .is\_valid \textbar{} .clean\_fields ↓
|
||
.clean\_fields\_machin
|
||
|
||
A compléter ;-)
|
||
|
||
\hypertarget{_duxe9pendance_avec_le_moduxe8le}{%
|
||
\subsection{Dépendance avec le
|
||
modèle}\label{_duxe9pendance_avec_le_moduxe8le}}
|
||
|
||
Un \textbf{form} peut dépendre d'une autre classe Django. Pour cela, il
|
||
suffit de fixer l'attribut \texttt{model} au niveau de la
|
||
\texttt{class\ Meta} dans la définition.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django }\ImportTok{import}\NormalTok{ forms}
|
||
|
||
\ImportTok{from}\NormalTok{ wish.models }\ImportTok{import}\NormalTok{ Wishlist}
|
||
|
||
\KeywordTok{class}\NormalTok{ WishlistCreateForm(forms.ModelForm):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Wishlist}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}name\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
De cette manière, notre form dépendra automatiquement des champs déjà
|
||
déclarés dans la classe \texttt{Wishlist}. Cela suit le principe de
|
||
\texttt{DRY\ \textless{}don’t\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ qu’une\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ l’attribut\ \textasciigrave{}fields},
|
||
nous pourrons nous assurer de faire évoluer le formulaire en fonction du
|
||
modèle sur lequel il se base.
|
||
|
||
\hypertarget{_rendu_et_affichage}{%
|
||
\subsection{Rendu et affichage}\label{_rendu_et_affichage}}
|
||
|
||
Le formulaire permet également de contrôler le rendu qui sera appliqué
|
||
lors de la génération de la page. Si les champs dépendent du modèle sur
|
||
lequel se base le formulaire, ces widgets doivent être initialisés dans
|
||
l'attribut \texttt{Meta}. Sinon, ils peuvent l'être directement au
|
||
niveau du champ.
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ date}
|
||
|
||
\ImportTok{from}\NormalTok{ django }\ImportTok{import}\NormalTok{ forms}
|
||
|
||
\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ Accident}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ AccidentForm(forms.ModelForm):}
|
||
\KeywordTok{class}\NormalTok{ Meta:}
|
||
\NormalTok{ model }\OperatorTok{=}\NormalTok{ Accident}
|
||
\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}gymnast\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}educative\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}date\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}information\textquotesingle{}}\NormalTok{)}
|
||
\NormalTok{ widgets }\OperatorTok{=}\NormalTok{ \{}
|
||
\StringTok{\textquotesingle{}date\textquotesingle{}}\NormalTok{ : forms.TextInput(}
|
||
\NormalTok{ attrs}\OperatorTok{=}\NormalTok{\{}
|
||
\StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}form{-}control\textquotesingle{}}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}data{-}provide\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}datepicker\textquotesingle{}}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}data{-}date{-}format\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}dd/mm/yyyy\textquotesingle{}}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}placeholder\textquotesingle{}}\NormalTok{ : date.today().strftime(}\StringTok{"}\SpecialCharTok{\%d}\StringTok{/\%m/\%Y"}\NormalTok{)}
|
||
\NormalTok{ \}),}
|
||
\StringTok{\textquotesingle{}information\textquotesingle{}}\NormalTok{ : forms.Textarea(}
|
||
\NormalTok{ attrs}\OperatorTok{=}\NormalTok{\{}
|
||
\StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}form{-}control\textquotesingle{}}\NormalTok{,}
|
||
\StringTok{\textquotesingle{}placeholder\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}Context (why, where, ...)\textquotesingle{}}
|
||
\NormalTok{ \})}
|
||
\NormalTok{ \}}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_squelette_par_duxe9faut}{%
|
||
\subsection{Squelette par défaut}\label{_squelette_par_duxe9faut}}
|
||
|
||
On a d'un côté le \{\{ form.as\_p \}\} ou \{\{ form.as\_table \}\}, mais
|
||
il y a beaucoup mieux que ça ;-) Voir les templates de Vitor et en
|
||
passant par \texttt{widget-tweaks}.
|
||
|
||
\hypertarget{_crispy_forms}{%
|
||
\subsection{Crispy-forms}\label{_crispy_forms}}
|
||
|
||
Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela
|
||
permet de valider des données reçues en entrée et d'afficher (très)
|
||
facilement des formulaires à compléter par l'utilisateur.
|
||
|
||
Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu
|
||
l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir,
|
||
modifier les types de contrôleurs, les placer au pixel près,
|
||
\ldots\hspace{0pt} Tout ça demande énormément de temps. Et c'est là
|
||
qu'intervient
|
||
\href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}.
|
||
Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation
|
||
et uni-form) et permet de contrôler entièrement le \textbf{layout} et la
|
||
présentation.
|
||
|
||
(c/c depuis le lien ci-dessous)
|
||
|
||
Pour chaque champ, crispy-forms va :
|
||
|
||
\begin{itemize}
|
||
\item
|
||
utiliser le \texttt{verbose\_name} comme label.
|
||
\item
|
||
vérifier les paramètres \texttt{blank} et \texttt{null} pour savoir si
|
||
le champ est obligatoire.
|
||
\item
|
||
utiliser le type de champ pour définir le type de la balise
|
||
\texttt{\textless{}input\textgreater{}}.
|
||
\item
|
||
récupérer les valeurs du paramètre \texttt{choices} (si présent) pour
|
||
la balise \texttt{\textless{}select\textgreater{}}.
|
||
\end{itemize}
|
||
|
||
\url{http://dotmobo.github.io/django-crispy-forms.html}
|
||
|
||
\hypertarget{_en_conclusion}{%
|
||
\subsection{En conclusion}\label{_en_conclusion}}
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Toute donnée entrée par l'utilisateur \textbf{doit} passer par une
|
||
instance de \texttt{form}.
|
||
\item
|
||
euh ?
|
||
\end{enumerate}
|
||
|
||
\hypertarget{_authentification}{%
|
||
\section{Authentification}\label{_authentification}}
|
||
|
||
Comme on l'a vu dans la partie sur le modèle, nous souhaitons que le
|
||
créateur d'une liste puisse retrouver facilement les éléments qu'il aura
|
||
créé. Ce dont nous n'avons pas parlé cependant, c'est la manière dont
|
||
l'utilisateur va pouvoir créer son compte et s'authentifier. La
|
||
\href{https://docs.djangoproject.com/en/stable/topics/auth/}{documentation}
|
||
est très complète, nous allons essayer de la simplifier au maximum.
|
||
Accrochez-vous, le sujet peut être complexe.
|
||
|
||
\hypertarget{_muxe9canisme_dauthentification}{%
|
||
\subsection{Mécanisme
|
||
d'authentification}\label{_muxe9canisme_dauthentification}}
|
||
|
||
On peut schématiser le flux d'authentification de la manière suivante :
|
||
|
||
En gros:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
La personne accède à une URL qui est protégée (voir les décorateurs
|
||
@login\_required et le mixin LoginRequiredMixin)
|
||
\item
|
||
Le framework détecte qu'il est nécessaire pour la personne de se
|
||
connecter (grâce à un paramètre type LOGIN\_URL)
|
||
\item
|
||
Le framework présente une page de connexion ou un mécanisme d'accès
|
||
pour la personne (template à définir)
|
||
\item
|
||
Le framework récupère les informations du formulaire, et les transmets
|
||
aux différents backends d'authentification, dans l'ordre
|
||
\item
|
||
Chaque backend va appliquer la méthode \texttt{authenticate} en
|
||
cascade, jusqu'à ce qu'un backend réponde True ou qu'aucun ne réponde
|
||
\item
|
||
La réponse de la méthode authenticate doit être une instance d'un
|
||
utilisateur, tel que définit parmi les paramètres généraux de
|
||
l'application.
|
||
\end{enumerate}
|
||
|
||
En résumé (bis):
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Une personne souhaite se connecter;
|
||
\item
|
||
Les backends d'authentification s'enchaîne jusqu'à trouver une bonne
|
||
correspondance. Si aucune correspondance n'est trouvée, on envoie la
|
||
personne sur les roses.
|
||
\item
|
||
Si OK, on retourne une instance de type current\_user, qui pourra être
|
||
utilisée de manière uniforme dans l'application.
|
||
\end{enumerate}
|
||
|
||
Ci-dessous, on définit deux backends différents pour mieux comprendre
|
||
les différentes possibilités:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Une authentification par jeton
|
||
\item
|
||
Une authentification LDAP
|
||
\end{enumerate}
|
||
|
||
\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}
|
||
|
||
\hypertarget{_tests}{%
|
||
\subsection{Tests}\label{_tests}}
|
||
|
||
\begin{quote}
|
||
Tests are part of the system.
|
||
|
||
--- Robert C. Martin Clean Architecture
|
||
\end{quote}
|
||
|
||
\hypertarget{_types_de_tests}{%
|
||
\subsubsection{Types de tests}\label{_types_de_tests}}
|
||
|
||
Les \textbf{tests unitaires} ciblent typiquement une seule fonction,
|
||
classe ou méthode, de manière isolée, en fournissant au développeur
|
||
l'assurance que son code réalise ce qu'il en attend. Pour plusieurs
|
||
raisons (et notamment en raison de performances), les tests unitaires
|
||
utilisent souvent des données stubbées - pour éviter d'appeler le "vrai"
|
||
service.
|
||
|
||
\begin{quote}
|
||
The aim of a unit test is to show that a single part of the application
|
||
does what programmer intends it to.
|
||
\end{quote}
|
||
|
||
Les \textbf{tests d'acceptance} vérifient que l'application fonctionne
|
||
comme convenu, mais à un plus haut niveau (fonctionnement correct d'une
|
||
API, validation d'une chaîne d'actions effectuées par un humain,
|
||
\ldots\hspace{0pt}).
|
||
|
||
\begin{quote}
|
||
The objective of acceptance tests is to prove that our application does
|
||
what the customer meant it to.
|
||
\end{quote}
|
||
|
||
Les \textbf{tests d'intégration} vérifient que l'application coopère
|
||
correctement avec les systèmes périphériques.
|
||
|
||
De manière plus générale, si nous nous rendons compte que les tests sont
|
||
trop compliqués à écrire ou nous coûtent trop de temps, c'est sans doute
|
||
que l'architecture de la solution n'est pas adaptée et que les
|
||
composants sont couplés les uns aux autres. Dans ces cas, il sera
|
||
nécessaire de refactoriser le code, afin que chaque module puisse être
|
||
testé indépendamment des autres. cite:{[}clean\_architecture{]}
|
||
|
||
\begin{quote}
|
||
Martin Fowler observes that, in general, "a ten minute build {[}and test
|
||
process{]} is perfectly within reason\ldots\hspace{0pt} {[}We first{]}
|
||
do the compilation and run tests that are more localized unit tests with
|
||
the database completely stubbed out. Such tests can run very fast,
|
||
keeping within the ten minutes guideline. However any bugs that involve
|
||
larger scale intercations, particularly those involving the real
|
||
database, won't be found. The second stage build runs a different suite
|
||
of tests {[}acceptance tests{]} that do hit the real database and
|
||
involve more end-to-end behavior. This suite may take a couple of hours
|
||
to run.
|
||
|
||
--- Robert C. Martin Clean Architecture
|
||
\end{quote}
|
||
|
||
Au final, le plus important est de toujours corréler les phases de tests
|
||
indépendantes du reste du travail (de développement, ici), en
|
||
l'automatisant au plus près de sa source de création.
|
||
|
||
En résumé, il est recommandé de:
|
||
|
||
\begin{enumerate}
|
||
\def\labelenumi{\arabic{enumi}.}
|
||
\item
|
||
Tester que le nommage d'une URL (son attribut \texttt{name} dans les
|
||
fichiers \texttt{urls.py}) corresponde à la fonction que l'on y a
|
||
définie
|
||
\item
|
||
Tester que l'URL envoie bien vers l'exécution d'une fonction (et que
|
||
cette fonction est celle que l'on attend)
|
||
\end{enumerate}
|
||
|
||
TODO: Voir comment configurer une \texttt{memoryDB} pour l'exécution des
|
||
tests.
|
||
|
||
\hypertarget{_tests_de_nommage}{%
|
||
\subsubsection{Tests de nommage}\label{_tests_de_nommage}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse}
|
||
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ HomeTests(TestCase):}
|
||
\KeywordTok{def}\NormalTok{ test\_home\_view\_status\_code(}\VariableTok{self}\NormalTok{):}
|
||
\NormalTok{ url }\OperatorTok{=}\NormalTok{ reverse(}\StringTok{"home"}\NormalTok{)}
|
||
\NormalTok{ response }\OperatorTok{=} \VariableTok{self}\NormalTok{.client.get(url)}
|
||
\VariableTok{self}\NormalTok{.assertEquals(response.status\_code, }\DecValTok{200}\NormalTok{)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_tests_durls}{%
|
||
\subsubsection{Tests d'urls}\label{_tests_durls}}
|
||
|
||
\begin{Shaded}
|
||
\begin{Highlighting}[]
|
||
\ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse}
|
||
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
|
||
|
||
\ImportTok{from}\NormalTok{ .views }\ImportTok{import}\NormalTok{ home}
|
||
|
||
|
||
\KeywordTok{class}\NormalTok{ HomeTests(TestCase):}
|
||
\KeywordTok{def}\NormalTok{ test\_home\_view\_status\_code(}\VariableTok{self}\NormalTok{):}
|
||
\NormalTok{ view }\OperatorTok{=}\NormalTok{ resolve(}\StringTok{"/"}\NormalTok{)}
|
||
\VariableTok{self}\NormalTok{.assertEquals(view.func, home)}
|
||
\end{Highlighting}
|
||
\end{Shaded}
|
||
|
||
\hypertarget{_conclusions_2}{%
|
||
\section{Conclusions}\label{_conclusions_2}}
|
||
|
||
\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}
|
||
|
||
Unresolved directive in part-5-go-live/\_index.adoc -
|
||
include::legacy/\_main.adoc{[}{]}
|
||
|
||
\hypertarget{_glossaire}{%
|
||
\section{Glossaire}\label{_glossaire}}
|
||
|
||
\begin{description}
|
||
\item[http]
|
||
\emph{HyperText Transfer Protocol}, ou plus généralement le protocole
|
||
utilisé (et détourné) pour tout ce qui touche au \textbf{World Wide
|
||
Web}. Il existe beaucoup d'autres protocoles d'échange de données, comme
|
||
\href{https://fr.wikipedia.org/wiki/Gopher}{Gopher},
|
||
\href{https://fr.wikipedia.org/wiki/File_Transfer_Protocol}{FTP} ou
|
||
\href{https://fr.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol}{SMTP}.
|
||
\item[IaaS]
|
||
\emph{Infrastructure as a Service}, où un tiers vous fournit des
|
||
machines (généralement virtuelles) que vous devrez ensuite gérer en bon
|
||
père de famille. L'IaaS propose souvent une API, qui vous permet
|
||
d'intégrer la durée de vie de chaque machine dans vos flux - en créant,
|
||
augmentant, détruisant une machine lorsque cela s'avère nécessaire.
|
||
\item[MVC]
|
||
Le modèle \emph{Model-View-Controler} est un patron de conception
|
||
autorisant un faible couplage entre la gestion des données (le
|
||
\emph{Modèle}), l'affichage et le traitement de celles (la \emph{Vue})
|
||
et la glue entre ces deux composants (au travers du \emph{Contrôleur}).
|
||
\href{https://en.wikipedia.org/wiki/Model\%E2\%80\%93view\%E2\%80\%93controller}{Wikipédia}
|
||
\item[ORM]
|
||
\emph{Object Relational Mapper}, où une instance est directement (ou à
|
||
proximité) liée à un mode de persistance de données.
|
||
\item[PaaS]
|
||
\emph{Platform as a Service}, qui consiste à proposer les composants
|
||
d'une plateforme (Redis, PostgreSQL, \ldots\hspace{0pt}) en libre
|
||
service et disponibles à la demande (quoiqu'après avoir communiqué son
|
||
numéro de carte de crédit\ldots\hspace{0pt}).
|
||
\item[POO]
|
||
La \emph{Programmation Orientée Objet} est un paradigme de programmation
|
||
informatique. Elle consiste en la définition et l'interaction de briques
|
||
logicielles appelées objets ; un objet représente un concept, une idée
|
||
ou toute entité du monde physique, comme une voiture, une personne ou
|
||
encore une page d'un livre. Il possède une structure interne et un
|
||
comportement, et il sait interagir avec ses pairs. Il s'agit donc de
|
||
représenter ces objets et leurs relations ; l'interaction entre les
|
||
objets via leurs relations permet de concevoir et réaliser les
|
||
fonctionnalités attendues, de mieux résoudre le ou les problèmes. Dès
|
||
lors, l'étape de modélisation revêt une importance majeure et nécessaire
|
||
pour la POO. C'est elle qui permet de transcrire les éléments du réel
|
||
sous forme virtuelle.
|
||
\href{https://fr.wikipedia.org/wiki/Programmation_orient\%C3\%A9e_objet}{Wikipédia}
|
||
\item[S3]
|
||
Amazon \emph{Simple Storage Service} consiste en un système
|
||
d'hébergement de fichiers, quels qu'ils soient. Il peut s'agir de
|
||
fichiers de logs, de données applications, de fichiers média envoyés par
|
||
vos utilisateurs, de vidéos et images ou de données de sauvegardes.
|
||
\end{description}
|
||
|
||
\textbf{\url{https://aws.amazon.com/fr/s3/}.}
|
||
|
||
\includegraphics{images/amazon-s3-arch.png}
|
||
|
||
\hypertarget{_bibliographie}{%
|
||
\section{Bibliographie}\label{_bibliographie}}
|
||
|
||
bibliography::{[}{]}
|
||
|
||
\end{document}
|