From ee76783f86362e7c95aa64c3ad2cb3f68548b832 Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Mon, 25 Apr 2022 19:12:16 +0200 Subject: [PATCH] Working a little bit on migrations, models, etc. --- asciidoc-to-tex.tex | 2698 +---------------- chapters/administration.tex | 40 + chapters/migrations.tex | 522 ++++ chapters/models.tex | 574 ++++ chapters/new-project.tex | 521 +++- chapters/python.tex | 6 + chapters/tests.tex | 42 +- chapters/working-in-isolation.tex | 229 ++ .../books-foreign-keys-example | 0 .../books-foreign-keys-example.drawio.png | Bin docker-miktex.sh | 1 + glossary.tex | 55 + main.tex | 22 +- parts/principles.tex | 29 + 14 files changed, 2026 insertions(+), 2713 deletions(-) create mode 100644 chapters/administration.tex create mode 100644 chapters/migrations.tex create mode 100644 chapters/models.tex rename {source/diagrams => diagrams}/books-foreign-keys-example (100%) rename {source/diagrams => diagrams}/books-foreign-keys-example.drawio.png (100%) create mode 100644 glossary.tex create mode 100644 parts/principles.tex diff --git a/asciidoc-to-tex.tex b/asciidoc-to-tex.tex index aafd341..d468111 100644 --- a/asciidoc-to-tex.tex +++ b/asciidoc-to-tex.tex @@ -1,673 +1,5 @@ -\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}[] @@ -702,238 +34,13 @@ La structure de vos répertoires devient celle-ci: \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}[] @@ -966,1569 +73,12 @@ Nous avons donc la structure finale pour notre environnement de travail: \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}} @@ -2537,70 +87,7 @@ Pour repartir de notre exemple ci-dessus, nous avions un modèle reprenant quelq 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: +va la faire quand même: la raison principale est que c \begin{Shaded} \begin{Highlighting}[] @@ -3524,123 +1011,6 @@ l'application. \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 @@ -6787,68 +4157,4 @@ Et pouvoir encoder les points des contrôles. \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} diff --git a/chapters/administration.tex b/chapters/administration.tex new file mode 100644 index 0000000..7f5a483 --- /dev/null +++ b/chapters/administration.tex @@ -0,0 +1,40 @@ +\chapter{Administration} + +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. 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. +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} + +\section{Le modèle de données} + +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: + diff --git a/chapters/migrations.tex b/chapters/migrations.tex new file mode 100644 index 0000000..8333516 --- /dev/null +++ b/chapters/migrations.tex @@ -0,0 +1,522 @@ +\chapter{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. \index{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{minted}{sql} + ALTER TABLE WishList ADD COLUMN Description nvarchar(MAX); +\end{minted} + +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{minted}{sql} + -- Firebird + ALTER TABLE Category ALTER COLUMN Name type varchar(2000) + + -- MSSQL + ALTER TABLE Category ALTER Column Name varchar(2000) + + -- Oracle + ALTER TABLE Category MODIFY Name varchar2(2000 +\end{minted} + +En bref, les problèmes suivants apparaissent très rapidement: + + +\begin{enumerate} + \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} + +\section{Fonctionnement général} + +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{minted}{python} +class Category(models.Model): + name = models.CharField(max_length=255) + + +class Book(models.Model): + title = models.CharField(max_length=255) + category = models.ForeignKey(Category, on_delete=models.CASCADE) +\end{minted} + +Nous avions ensuite modifié la clé de liaison, pour permettre d'associer plusieurs catégories à un même livre, et inversément: + +\begin{minted}{python} +class Category(models.Model): + name = models.CharField(max_length=255) + + +class Book(models.Model): + title = models.CharField(max_length=255) + category = models.ManyManyField(Category, on_delete=models.CASCADE) +\end{minted} + + +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{minted}{python} +# library/migrations/0001_initial.py +from django.db import migrations, models +import django.db.models.deletion +class Migration(migrations.Migration): + initial = True + dependencies = [] + operations = [ + migrations.CreateModel( + name="Category", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name="Book", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField(max_length=255) + ), + ( + "category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="library.category", + ), + ), + ], + ), + ] +\end{minted} + +\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{minted}{python} +class Category(models.Model): + name = models.CharField(max_length=255) + + +class Book(models.Model): + title = models.CharField(max_length=255) + category = models.ManyManyField(Category) +\end{minted} + +Vous noterez que l'attribut \texttt{on\_delete} n'est plus nécessaire. + +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. +Ce qu'applique la migration en supprimant le champ liant initialement un livre à une catégorie et en ajoutant une nouvelle table de liaison. + +\begin{minted}{python} + # library/migrations/0002_remove_book_category_book_category.py +from django.db import migrations, models +class Migration(migrations.Migration): + dependencies = [ + ('library', '0001_initial'), + ] + operations = [ + migrations.RemoveField( + model_name='book', + name='category', + ), + migrations.AddField( + model_name='book', + name='category', + field=models.ManyToManyField(to='library.Category'), + ), + ] +\end{minted} + + +\begin{itemize} + \item + La migration supprime l'ancienne clé étrangère ... + \item + ... 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} + +\section{Graph de dépendances} + +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{minted}{python} + [snip] + INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + ] + [snip] +\end{minted} + +Dès que nous les appliquerons, nous recevrons les messages suivants: + +\begin{verbatim} + $ python manage.py migrate + + Operations to perform: + Apply all migrations: admin, auth, contenttypes, library, sessions, world + Running migrations: + Applying contenttypes.0001_initial... OK + Applying auth.0001_initial... OK + Applying admin.0001_initial... OK + Applying admin.0002_logentry_remove_auto_add... OK + Applying admin.0003_logentry_add_action_flag_choices... OK + Applying contenttypes.0002_remove_content_type_name... OK + Applying auth.0002_alter_permission_name_max_length... OK + Applying auth.0003_alter_user_email_max_length... OK + Applying auth.0004_alter_user_username_opts... OK + Applying auth.0005_alter_user_last_login_null... OK + Applying auth.0006_require_contenttypes_0002... OK + Applying auth.0007_alter_validators_add_error_messages... OK + Applying auth.0008_alter_user_username_max_length... OK + Applying auth.0009_alter_user_last_name_max_length... OK + Applying auth.0010_alter_group_name_max_length... OK + Applying auth.0011_update_proxy_permissions... OK + Applying auth.0012_alter_user_first_name_max_length... OK + Applying sessions.0001_initial... OK +\end{verbatim} + +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} + +\section{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} +\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{minted}{python} +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + +class Migration(migrations.Migration): + dependencies = [ + ('gwift', '0004_name_value'), + ] + operations = [ + migrations.AddField( + model_name='wishlist', + name='description', + field=models.TextField(default="", null=True) + preserve_default=False, + ), + ] +\end{minted} + +\section{Liste des migrations appliquées} + +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{verbatim} + $ python manage.py showmigrations + admin + [X] 0001_initial + [X] 0002_logentry_remove_auto_add + [X] 0003_logentry_add_action_flag_choices + auth + [X] 0001_initial + [X] 0002_alter_permission_name_max_length + [X] 0003_alter_user_email_max_length + [X] 0004_alter_user_username_opts + [X] 0005_alter_user_last_login_null + [X] 0006_require_contenttypes_0002 + [X] 0007_alter_validators_add_error_messages + [X] 0008_alter_user_username_max_length + [X] 0009_alter_user_last_name_max_length + [X] 0010_alter_group_name_max_length + [X] 0011_update_proxy_permissions + [X] 0012_alter_user_first_name_max_length + contenttypes + [X] 0001_initial + [X] 0002_remove_content_type_name + library + [X] 0001_initial + [X] 0002_remove_book_category_book_category + [ ] 0003_book_summary + sessions + [X] 0001_initial +\end{verbatim} + +\section{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{minted}{python} +# library/migrations/0002_remove_book_category.py + +from django.db import migrations, models + +class Migration(migrations.Migration): + dependencies = [ + ('library', '0001_initial'), + ] + operations = [ + migrations.RemoveField( + model_name='book', + name='category', + ), + migrations.AddField( + model_name='book', + name='category', + field=models.ManyToManyField(to='library.Category'), + ), + ] +\end{minted} + +\begin{minted}{python} +# library/migrations/0003_book_summary.py + +from django.db import migrations, models + +class Migration(migrations.Migration): + dependencies = [ + ('library', '0002_remove_book_category_book_category'), + ] + operations = [ + migrations.AddField( + model_name='book', + name='summary', + field=models.TextField(blank=True), + ), + ] +\end{minted} + +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{verbatim} + $ python manage.py squashmigrations library 0002 0003 + + Will squash the following migrations: + - 0002_remove_book_category_book_category + - 0003_book_summary + Do you wish to proceed? [yN] y + + Optimizing... + + No optimizations possible. + + Created new squashed migration + /home/fred/Sources/gwlib/library/migrations/0002_remove_book_category_book_cat + egory_squashed_0003_book_summary.py + + You should commit this migration but leave the old ones in place; + the new migration will be used for new installs. Once you are sure + all instances of the codebase have applied the migrations you squashed, + you can delete them +\end{verbatim} + + +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{minted}{python} +$ cat +library/migrations/0002_remove_book_category_book_category_squashed_0003_book_ +summary.py + +# Generated by Django 4.0.3 on 2022-03-15 18:01 + +from django.db import migrations, models + +class Migration(migrations.Migration): + replaces = [ + ('library', '0002_remove_book_category_book_category'), + ('library', '0003_book_summary')] + dependencies = [ + ('library', '0001_initial'), + ] + operations = [ + migrations.RemoveField( + model_name='book', + name='category', + ), + migrations.AddField( + model_name='book', + name='category', + field=models.ManyToManyField(to='library.category'), + ), + migrations.AddField( + model_name='book', + name='summary', + field=models.TextField(blank=True), + ), + ] +\end{minted} + +\section{Réinitialisation de 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} diff --git a/chapters/models.tex b/chapters/models.tex new file mode 100644 index 0000000..32bb3f7 --- /dev/null +++ b/chapters/models.tex @@ -0,0 +1,574 @@ +\chapter{Modélisation} + +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} +\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. + +\section{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{minted}{Python} + class Square: + def __init__(self, top_left, side): + self.top_left = top_left + self.side = side + + class Rectangle: + def __init__(self, top_left, height, width): + self.top_left = top_left + self.height = height + self.width = width + + class Circle: + def __init__(self, center, radius): + self.center = center + self.radius = radius +\end{minted} + +Si nous souhaitons ajouter une fonctionnalité permettant de calculer l'aire pour chacune de ces structures, nous aurons deux possibilités: + +\begin{enumerate} +\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{minted}{python} +class Geometry: + PI = 3.141592653589793 + + def area(self, shape): + if isinstance(shape, Square): + return shape.side * shape.side + + if isinstance(shape, Rectangle): + return shape.height * shape.width + + if isinstance(shape, Circle): + return PI * shape.radius**2 + + raise NoSuchShapeException() +\end{minted} + +Dans le second cas, l'implémentation pourrait évoluer de la manière suivante: + +\begin{minted}{python} +class Shape: + def area(self): + pass + +class Square(Shape): + def __init__(self, top_left, side): + self.__top_left = top_left + self.__side = side + + def area(self): + return self.__side * self.__side + +class Rectangle(Shape): + def __init__(self, top_left, height, width): + self.__top_left = top_left + self.__height = height + self.__width = width + + def area(self): + return self.__height * self.__width + +class Circle(Shape): + def __init__(self, center, radius): + self.__center = center + self.__radius = radius + + def area(self): + PI = 3.141592653589793 + return PI * self.__radius**2 +\end{minted} + +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[pp. 95-97]{clean_code}, 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()}, ... +En résumé, toutes les méthodes qui font qu'une instance sait \textbf{comment} interagir avec la base de données. + +\section{Types de champs, relations et clés étrangères} + +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{minted}{python} +class Category(models.Model): + name = models.CharField(max_length=255) + +class Book(models.Model): + title = models.CharField(max_length=255) + category = models.ForeignKey(Category, on_delete=models.CASCADE) +\end{minted} + + +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{minted}{python} +from library.models import Book, Category + +movies = Category.objects.create(name="Adaptations au cinéma") +medieval = Category.objects.create(name="Médiéval-Fantastique") +science_fiction = Category.objects.create(name="Sciences-fiction") +computers = Category.objects.create(name="Sciences Informatiques") + +books = { + "Harry Potter": movies, + "The Great Gatsby": movies, + "Dune": science_fiction, + "H2G2": science_fiction, + "Ender's Game": science_fiction, + "Le seigneur des anneaux": medieval, + "L'Assassin Royal", medieval, + "Clean code": computers, + "Designing Data-Intensive Applications": computers +} + +for book_title, category in books.items: + Book.objects.create(name=book_title, category=category) +\end{minted} + +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{minted}{python} +class Category(models.Model): + name = models.CharField(max_length=255) + +class Book(models.Model): + title = models.CharField(max_length=255) + category = models.ManyManyField(Category, on_delete=models.CASCADE) +\end{minted} + +Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition. + +\subsection{Accès aux relations} + +\begin{minted}{python} +# wish/models.py +class Wishlist(models.Model): + pass + +class Item(models.Model): + wishlist = models.ForeignKey(Wishlist) +\end{minted} + +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{minted}{python} +# wish/models.py +class Wishlist(models.Model): + pass + +class Item(models.Model): + wishlist = models.ForeignKey(Wishlist, related_name='items') +\end{minted} + +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{verbatim} + # python manage.py shell + + >>> from wish.models import Wishlist, Item + >>> wishlist = Wishlist.create('Liste de test', 'description') + >>> item = Item.create('Element de test', 'description', w) + >>> + >>> item.wishlist + + >>> + >>> wishlist.items.all() + [] +\end{verbatim} + + +\subsection{Choix} + +Voir \href{https://girlthatlovestocode.com/django-model}{ici} + +\begin{minted}{python} +class Runner(models.Model): + + # this is new: + class Zone(models.IntegerChoices): + ZONE_1 = 1, 'Less than 3.10' + ZONE_2 = 2, 'Less than 3.25' + ZONE_3 = 3, 'Less than 3.45' + ZONE_4 = 4, 'Less than 4 hours' + ZONE_5 = 5, 'More than 4 hours' + + name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + email = models.EmailField() + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + start_zone = models.PositiveSmallIntegerField( # this is new + choices=Zone.choices, + default=Zone.ZONE_5, + help_text="What was your best time on the marathon in last 2 years?" + ) +\end{minted} + +\section{Validateurs} + +\section{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. +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{minted}{python} +class ItemManager(...): + (de mémoire, je ne sais plus exactement) +\end{minted} + +\section{Jointures, compositions et filtres} + +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\ (...)}. +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... + +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{minted}{python} +informations = ( + .objects.filter() + .select_related() + .prefetch_related() + .iterator(chunk_size=1000) +) +\end{minted} + + +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. + + + +\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{minted}{python} +from core.models import Wish + +Wish.objects +Wish.objects.filter(name__icontains="test").filter(name__icontains="too") +\end{minted} + + +\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{minted}{python} +from django.db.models import Q + +condition1 = Q(...) +condition2 = Q(...) + +bidule.objects.filter(condition1 | condition2) +\end{minted} + +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; + + +\section{Optimisation} + + +\subsection{N+1 Queries} + +\begin{enumerate} + \def\labelenumi{\arabic{enumi}.} + \item + Prefetch + \item + select\_related +\end{enumerate} + + +\subsection{Unicité} + + +\subsection{Indices} + +\section{Agrégation et annotations} + +\url{https://docs.djangoproject.com/en/3.1/topics/db/aggregation/} + +\section{Métamodèle 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{minted}{python} +class Wish(models.Model): + name = models.CharField(max_length=255) + + class Meta: + ordering = ('name',) +\end{minted} + +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. + +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{minted}{python} +constraints = [ # constraints added + models.CheckConstraint(check=models.Q(year_born__lte=datetime.date + .today().year-18), name='will_be_of_age'), +] +\end{minted} + +\section{Querysets et 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 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{verbatim} + queryset = Wishlist.objects.all() + print(queryset.query) +\end{verbatim} + + +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} + + +\section{Conclusions} + +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. + +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. + diff --git a/chapters/new-project.tex b/chapters/new-project.tex index 882cb15..ce81a12 100644 --- a/chapters/new-project.tex +++ b/chapters/new-project.tex @@ -4,18 +4,498 @@ 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}). +\section{Gestion des dépendances} + +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{verbatim} + $ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift- + env/Scrips/activate.bat pour Windows. + $ pip install django + Collecting django + Downloading Django-3.1.4 + 100% |################################| + Installing collected packages: django + Successfully installed django-3.1.4 +\end{verbatim} + + +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{verbatim} + $ django-admin startproject gwift +\end{verbatim} + +Cette action a pour effet de créer un nouveau dossier \texttt{gwift}, +dans lequel nous trouvons la structure suivante: + +\begin{verbatim} + $ tree gwift + gwift + -- gwift + ----- asgi.py + ----- __init__.py + ----- settings.py + ----- urls.py + ----- wsgi.py + -- manage.py +\end{verbatim} + +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: + +TODO + +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 égale à la version 3.2 dans le fichier \texttt{requirements/base.txt}. + +\begin{verbatim} + $ echo 'django==3.2' > requirements/base.txt + $ echo '-r base.txt' > requirements/prod.txt + $ echo '-r base.txt' > requirements/dev.txt +\end{verbatim} + +Une bonne pratique consiste à également placer un fichier \texttt{requirements.txt} à la racine du projet, et dans lequel nous retrouverons le contenu \texttt{-r requirements/production.txt} (notamment pour Heroku). + +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 parcourirles 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. + + \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). +\section{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} +\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}, ... + +\subsection{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, ... +\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{verbatim} + $ python manage.py runserver + Watching for file changes with StatReloader + Performing system checks... + System check identified no issues (0 silenced). + [...] + December 15, 2020 - 20:45:07 + Django version 3.1.4, using settings 'gwift.settings' + Starting development server at http://127.0.0.1:8000/ + Quit the server with CTRL-BREAK. +\end{verbatim} + +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. + +\section{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{verbatim} + $ python manage.py startapp wish +\end{verbatim} + +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: + +TODO + + +Notre application a bien été créée, et nous l'avons déplacée dans le répertoire \texttt{gwift} ! + +\section{Fonctionnement général} + +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, ... +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{minted}{Python} + from django.contrib import admin + from django.urls import path + from gwift.views import wish_details + + urlpatterns = [ + path('admin/', admin.site.urls), + path("wishes/", wish_details), + ] +\end{minted} + + +\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{minted}{Python} + [...] + + from datetime import datetime + + + def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse: + context = { + "user_name": "Bond," + "user_first_name": "James", + "now": datetime.now() + } + return render( + request, + "wish_details.html", + context + ) +\end{minted} + + +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{minted}{html} + + + + + Page title + + +

Hi!

+

My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.

+

This page was generated at {{ now }}

+ + +\end{minted} + +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{minted}{html} + + + + Page title + + +

Hi!

+

My name is Bond. James Bond.

+

This page was generated at 2027-03-19 19:47:38

+ + +\end{minted} + +\begin{figure} + \centering + \includegraphics{images/django/django-first-template.png} + \caption{Résultat} +\end{figure} + +\section{Configuration globale} + +\subsection{setup.cfg} + +→ Faire le lien avec les settings → Faire le lien avec les douze +facteurs → Construction du fichier setup.cfg + +\begin{verbatim} + [flake8] + max-line-length = 100 + exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv + + [pycodestyle] + max-line-length = 100 + exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv + + [mypy] + python_version = 3.8 + check_untyped_defs = True + ignore_missing_imports = True + warn_unused_ignores = True + warn_redundant_casts = True + warn_unused_configs = True + plugins = mypy_django_plugin.main + + [mypy.plugins.django-stubs] + django_settings_module = config.settings.test + + [mypy-*.migrations.*] + # Django migrations should not produce any errors: + ignore_errors = True + + [coverage:run] + include = khana/* + omit = *migrations*, *tests* + plugins = + django_coverage_plugin +\end{verbatim} + +\subsection{Structure finale} + +Nous avons donc la structure finale pour notre environnement de travail: + +\subsection{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, ... + +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{verbatim} + python -m venv .venvs\cookie-cutter-khana + .venvs\cookie-cutter-khana\Scripts\activate.bat + + (cookie-cutter-khana) $ pip install cookiecutter + Collecting cookiecutter + [...] + Successfully 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 + (cookie-cutter-khana) $ cookiecutter https://github.com/pydanny/cookiecutter- + django + [...] + [SUCCESS]: Project initialized, keep up the good work! +\end{verbatim} + +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{verbatim} + django-admin.py startproject --template=https://[...].zip +\end{verbatim} \section{Tests unitaires} Chaque application est créée par défaut avec un fichier \textbf{tests.py}, qui inclut la classe \texttt{TestCase} depuis le package \texttt{django.test}: - On a deux choix ici: \begin{enumerate} @@ -91,3 +571,42 @@ La configuration peut se faire dans un fichier .coveragerc que vous placerez à $ coverage html \end{verbatim} + +\subsection{Réalisation des tests} + +En résumé, il est recommandé de: + +\begin{enumerate} +\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} + +\subsubsection{Tests de nommage} + +\begin{minted}{python} +from django.core.urlresolvers import reverse +from django.test import TestCase + +class HomeTests(TestCase): + def test_home_view_status_code(self): + url = reverse("home") + response = self.client.get(url) + self.assertEquals(response.status_code, 200) +\end{minted} + +\subsubsection{Tests d'URLs} + +\begin{minted}{python} + from django.core.urlresolvers import reverse + from django.test import TestCase + + from .views import home + + class HomeTests(TestCase): + def test_home_view_status_code(self): + view = resolve("/") + self.assertEquals(view.func, home) +\end{minted} + diff --git a/chapters/python.tex b/chapters/python.tex index c306cd3..56fa370 100644 --- a/chapters/python.tex +++ b/chapters/python.tex @@ -657,6 +657,12 @@ Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer p Ajoutez-le dans le fichier \texttt{requirements/base.txt}, et lancez une couverture de code grâce à la commande \texttt{coverage}. La configuration peut se faire dans un fichier \texttt{.coveragerc} que vous placerez à la racine de votre projet, et qui sera lu lors de l'exécution. +\section{Gestion des versions de l'interpréteur} + +\begin{verbatim} + pyenv install 3.10 +\end{verbatim} + \section{Matrice de compatibilité} L'intérêt de la matrice de compatibilité consiste à spécifier un ensemble de plusieurs versions d'un même interpréteur (ici, Python), afin de s'assurer que votre application continue à fonctionner. diff --git a/chapters/tests.tex b/chapters/tests.tex index 945d8db..f475eef 100644 --- a/chapters/tests.tex +++ b/chapters/tests.tex @@ -109,8 +109,33 @@ Une solution serait de passer par un dictionnaire, de façon à ramener la compl \caption{La même version, avec une complexité réduite à 1} \end{listing} +\section{Types de tests} -\section{Tests unitaires} +De manière 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_code} + +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: + +\begin{quote} +Martin Fowler observes that, in general, "a ten minute build [and test process] is perfectly within reason... +[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} + +\subsection{Tests unitaires} + +\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 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 Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. @@ -129,6 +154,19 @@ vérifier que le code est \textbf{bien} testé, mais juste de vérifier \textbf{quelle partie} du code est testée. Le paquet \texttt{coverage} se charge d'évaluer le pourcentage de code couvert par les tests. +\subsection{Tests d'acceptance} -\section{Tests d'intégration} +\begin{quote} + The objective of acceptance tests is to prove that our application does + what the customer meant it to. +\end{quote} + +Les 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, ...). + +\subsection{Tests d'intégration} + +Les tests d’intégration vérifient que l’application coopère correctement avec les systèmes +périphériques diff --git a/chapters/working-in-isolation.tex b/chapters/working-in-isolation.tex index fb75b58..fe824a8 100644 --- a/chapters/working-in-isolation.tex +++ b/chapters/working-in-isolation.tex @@ -76,7 +76,236 @@ TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à test_django_gecko.py 2 directories, 5 files \end{verbatim} + + +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{verbatim} + mkdir ~/.venvs/ + python -m venv ~/.venvs/gwift-venv +\end{verbatim} + +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{verbatim} + # GNU/Linux, macOS + source ~/.venvs/gwift-venv/bin/activate + # MS Windows, avec Cmder + ~/.venvs/gwift-venv/Scripts/activate.bat + # Pour les deux + (gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$ +\end{verbatim} + +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}. + +\subsubsection{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} +\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. + \section{Un système de virtualisation} Par "\emph{système de virtualisation}", nous entendons n'importe quel application, système d'exploitation, système de containeurisation, ... qui permette de créer ou recréer un environnement de diff --git a/source/diagrams/books-foreign-keys-example b/diagrams/books-foreign-keys-example similarity index 100% rename from source/diagrams/books-foreign-keys-example rename to diagrams/books-foreign-keys-example diff --git a/source/diagrams/books-foreign-keys-example.drawio.png b/diagrams/books-foreign-keys-example.drawio.png similarity index 100% rename from source/diagrams/books-foreign-keys-example.drawio.png rename to diagrams/books-foreign-keys-example.drawio.png diff --git a/docker-miktex.sh b/docker-miktex.sh index e3b3a63..10832f5 100755 --- a/docker-miktex.sh +++ b/docker-miktex.sh @@ -1,5 +1,6 @@ docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments pdflatex main.tex -shell-escape docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments makeindex main.idx docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments bibtex main +# docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments makeglossaries main docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments pdflatex main.tex -shell-escape diff --git a/glossary.tex b/glossary.tex new file mode 100644 index 0000000..0222856 --- /dev/null +++ b/glossary.tex @@ -0,0 +1,55 @@ +\chapter{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} + \ No newline at end of file diff --git a/main.tex b/main.tex index 5eddd9a..bffbfb2 100644 --- a/main.tex +++ b/main.tex @@ -1,7 +1,9 @@ \documentclass[twoside=no,parskip=half,numbers=enddot,bibliography=totoc,index=totoc,listof=totoc]{scrbook} \usepackage{makeidx} +\usepackage[utf8]{inputenc} \usepackage[french]{babel} \usepackage{csquotes} +\usepackage[acronym]{glossaries} \usepackage{hyperref} \usepackage{setspace} \usepackage{listing} @@ -11,8 +13,8 @@ \usepackage{float} \usepackage[export]{adjustbox} -\renewcommand\listlistingname{Liste des morceaux de code} +\renewcommand\listlistingname{Liste des morceaux de code} \onehalfspacing @@ -45,27 +47,17 @@ \include{parts/environment.tex} \include{chapters/maintenability.tex} - \include{chapters/architecture.tex} - \include{chapters/tests.tex} - \include{chapters/tools.tex} - \include{chapters/working-in-isolation.tex} - \include{chapters/python.tex} - \include{chapters/new-project.tex} +\include{parts/principles.tex} -\part{Principes fondamentaux de Django} - -\chapter{Modélisation} - -\chapter{Migrations} - -\chapter{Shell} +\include{chapters/models.tex} +\include{chapters/migrations.tex} \chapter{Administration} @@ -134,6 +126,8 @@ \bibliography{references} \bibliographystyle{plainnat} +\include{glossary.tex} + \include{chapters/thanks.tex} \end{document} diff --git a/parts/principles.tex b/parts/principles.tex new file mode 100644 index 0000000..36cffe7 --- /dev/null +++ b/parts/principles.tex @@ -0,0 +1,29 @@ +\part{Principes fondamenteux de Django} + +Dans cette partie, 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.