Code review and rewriting

This commit is contained in:
Gregory Trullemans 2022-05-01 19:45:53 +02:00 committed by Fred Pauchet
parent c15e05349d
commit a88f1854a0
16 changed files with 1047 additions and 1209 deletions

View File

@ -1,18 +1,19 @@
\chapter{Administration} \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. 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é. 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} \index{CRUD} \footnote{\emph{Create-Read-Update-Delete}, c'est-à-dire le fonctionnement par défaut de beaucoup d'applications}, c'est-à-dire tout ce qu'il faut pour ajouter, lister, modifier ou supprimer des informations. 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} \index{CRUD} \footnote{\emph{Create-Read-Update-Delete}, c'est-à-dire le fonctionnement par défaut de beaucoup d'applications}, 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. 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. 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. 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 interface 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). Cette interface 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. 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}. 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. 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. C'est aussi un excellent outil de prototypage et de preuve de concept.
@ -32,7 +33,8 @@ Elle se base sur plusieurs couches que l'on a déjà (ou on va bientôt) aborder
\section{Le modèle de données} \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. 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: 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:
@ -50,7 +52,7 @@ urlpatterns = [
Cette URL signifie que la partie \texttt{admin} est déjà active et accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}. Cette URL signifie que la partie \texttt{admin} est déjà active et accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}.
C'est le seul prérequis pour cette partie. C'est le seul prérequis pour cette partie.
Chaque application nouvellement créée contient par défaut un fichier \texttt{admin.py}, dans lequel il est possible de déclarer les ensembles de données seront accessibles ou éditables. Chaque application nouvellement créée contient par défaut un fichier \texttt{admin.py}, dans lequel il est possible de déclarer les ensembles de données seront accessibles ou éditables.
Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits: Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits:
\begin{minted}{python} \begin{minted}{python}
@ -83,107 +85,97 @@ quelques lignes de configuration dans ce fichier \texttt{admin.py}:
\begin{itemize} \begin{itemize}
\item \item
Nous importons les modèles que nous souhaitons gérer dans l'admin Nous importons les modèles que nous souhaitons gérer dans l'admin
\item \item
Et nous les déclarons comme gérables. Cette dernière ligne implique aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui n'activera simplement aucune opération de lecture ou modification. Et nous les déclarons comme gérables. Cette dernière ligne implique aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui n'activera simplement aucune opération de lecture ou modification.
\end{itemize} \end{itemize}
Il nous reste une seule étape à réaliser: créer un nouvel utilisateur. Il nous reste une seule étape à réaliser: créer un nouvel utilisateur.
Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}. Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}.
\begin{verbatim} \begin{verbatim}
$ python manage.py createsuperuser $ python manage.py createsuperuser
Username (leave blank to use 'fred'): fred Username (leave blank to use 'fred'): fred
Email address: fred@root.org Email address: fred@root.org
Password: ****** Password: ******
Password (again): ****** Password (again): ******
Superuser created successfully. Superuser created successfully.
\end{verbatim} \end{verbatim}
\begin{figure}[H] \begin{figure}[H]
\centering \centering
\includegraphics{images/django/django-site-admin.png} \includegraphics{images/django/django-site-admin.png}
\caption{Connexion au site d'administration} \caption{Connexion au site d'administration}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics{images/django/django-site-admin-after-connection.png}
\caption{Administration}
\end{figure} \end{figure}
Ceci nous permet déjà d'ajouter des éléments (Items), des listes de souhaits, de visualiser les actions récentes, voire de gérer les autorisations attribuées aux utilisateurs, comme les membres du staff ou les administrateurs. \begin{figure}[H]
\centering
\includegraphics{images/django/django-site-admin-after-connection.png}
\caption{Administration}
\end{figure}
Ceci nous permet déjà d'ajouter des éléments (Items), des listes de souhaits, de visualiser les actions récentes, voire de gérer les autorisations attribuées aux utilisateurs, comme les membres du staff ou les administrateurs.
\section{Quelques conseils de base} \section{Quelques conseils de base}
\begin{enumerate} \begin{enumerate}
\item \item
Surchargez la méthode \texttt{str(self)} pour chaque classe que vous aurez définie dans le modèle. Surchargez la méthode \texttt{str(self)} pour chaque classe que vous aurez définie dans le modèle.
Cela permettra de construire une représentation textuelle pour chaque instance de votre classe. Cela permettra de construire une représentation textuelle pour chaque instance de votre classe.
Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l'on manipule. Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l'on manipule.
En plus, cette méthode est également appelée lorsque l'administration historisera une action (et comme cette étape sera inaltérable, autant qu'elle soit fixée dans le début). En plus, cette méthode est également appelée lorsque l'administration historisera une action (et comme cette étape sera inaltérable, autant qu'elle soit fixée dans le début).
\item \item
La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle on peut accéder pour obtenir les détails d'une instance. Par exemple: La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle on peut accéder pour obtenir les détails d'une instance. Par exemple:
\end{enumerate} \end{enumerate}
\begin{minted}{python} \begin{minted}{python}
def get_absolute_url(self): def get_absolute_url(self):
return reverse('myapp.views.details', args=[self.id]) return reverse('myapp.views.details', args=[self.id])
\end{minted} \end{minted}
\begin{enumerate} \begin{enumerate}
\def\labelenumi{\arabic{enumi}.} \def\labelenumi{\arabic{enumi}.}
\item \item
Les attributs \texttt{Meta}: Les attributs \texttt{Meta}:
\end{enumerate} \end{enumerate}
\begin{minted}{python} \begin{minted}{python}
class Meta: class Meta:
ordering = ['-field1', 'field2'] ordering = ['-field1', 'field2']
verbose_name = 'my class in singular' verbose_name = 'my class in singular'
verbose_name_plural = 'my class when is in a list!' verbose_name_plural = 'my class when is in a list!'
\end{minted} \end{minted}
\begin{enumerate}
\item \begin{enumerate}
Le titre: \item
Le titre:
\begin{itemize} \begin{itemize}
\item \item
Soit en modifiant le template de l'administration Soit en modifiant le template de l'administration
\item \item
Soit en ajoutant l'assignation suivante dans le fichier Soit en ajoutant l'assignation suivante dans le fichier \texttt{urls.py} : \texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}.
\texttt{urls.py}:
\texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}.
\end{itemize} \end{itemize}
\item \item
Prefetch Prefetch
\end{enumerate} \end{enumerate}
\url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64} \url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64}
\url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614} \url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614}
En gros, le problème de l'admin est que si on fait des requêtes En gros, le problème de l'admin est que si on fait des requêtes imbriquées, on va flinguer l'application et le chargement de la page.
imbriquées, on va flinguer l'application et le chargement de la page. La La solution consiste à utiliser la propriété \texttt{list\_select\_related} de la classe d'Admin, afin d'appliquer une jointure par défaut et et gagner en performances.
solution consiste à utiliser la propriété \texttt{list\_select\_related}
de la classe d'Admin, afin d'appliquer une jointure par défaut et et
gagner en performances.
\subsection{admin.ModelAdmin} \subsection{admin.ModelAdmin}
La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement dans le fichier \texttt{admin.py} de chaque application contiendra la définition de ce que l'on souhaite faire avec nos données dans l'administration. Cette classe (et sa partie Meta)
dans le fichier \texttt{admin.py} de chaque application contiendra la
définition de ce que l'on souhaite faire avec nos données dans
l'administration. Cette classe (et sa partie Meta)
\subsection{L'affichage} \subsection{L'affichage}
Comme l'interface d'administration fonctionne (en trèèèès) gros comme un Comme l'interface d'administration fonctionne (en trèèèès) gros comme un CRUD auto-généré, on trouve par défaut la possibilité de :
CRUD auto-généré, on trouve par défaut la possibilité de :
\begin{enumerate} \begin{enumerate}
\item \item
@ -200,7 +192,7 @@ Les affichages sont donc de deux types: en liste et au détail.
Pour les affichages en liste, le plus simple consiste à jouer sur la propriété \texttt{list\_display}. Pour les affichages en liste, le plus simple consiste à jouer sur la propriété \texttt{list\_display}.
Par défaut, la première colonne va accueillir le lien vers le formulaire d'édition. Par défaut, la première colonne va accueillir le lien vers le formulaire d'édition.
On peut donc modifier ceci, voire créer de nouveaux liens vers d'autres éléments en construisant des URLs dynamiquement. On peut donc modifier ceci, voire créer de nouveaux liens vers d'autres éléments en construisant des URLs dynamiquement.
Voir aussi comment personnaliser le fil d'Ariane ? Voir aussi comment personnaliser le fil d'Ariane ?
@ -230,7 +222,7 @@ Chaque liste permet de spécifier des filtres spécifiques; ceux-ci peuvent êtr
\section{Permissions} \section{Permissions}
On l'a dit plus haut, il vaut mieux éviter de proposer un accès à l'administration à vos utilisateurs. On l'a dit plus haut, il vaut mieux éviter de proposer un accès à l'administration à vos utilisateurs.
Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression. Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression.
Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les méthodes suivantes: Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les méthodes suivantes:
@ -289,13 +281,13 @@ défaut, il existe déjà une action de \textbf{suppression}.
Les paramètres d'entrée sont : Les paramètres d'entrée sont :
\begin{enumerate} \begin{enumerate}
\def\labelenumi{\arabic{enumi}.} \def\labelenumi{\arabic{enumi}.}
\item \item
L'instance de classe L'instance de classe
\item \item
La requête entrante La requête entrante
\item \item
Le queryset correspondant à la sélection. Le queryset correspondant à la sélection.
\end{enumerate} \end{enumerate}
\begin{minted}{python} \begin{minted}{python}
@ -319,6 +311,6 @@ else:
\section{Documentation} \section{Documentation}
Nous l'avons dit plus haut, l'administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données. Nous l'avons dit plus haut, l'administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données.
Pour cela, il suffit de suivre les bonnes pratiques, puis \href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer la documentation à partir des URLs}: Pour cela, il suffit de suivre les bonnes pratiques, puis \href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer la documentation à partir des URLs}:

View File

@ -4,15 +4,13 @@
\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs \url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs
Django Rest Framework Django Rest Framework
Expliquer pourquoi une API est intéressante/primordiale/la première Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis.
chose à réaliser/le cadet de nos soucis.
Voir peut-être aussi Voir peut-être aussi
\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/} \url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/}
Au niveau du modèle, nous allons partir de quelque chose de très simple: Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service d'affectation.
des personnes, des contrats, des types de contrats, et un service Quelque chose comme ceci:
d'affectation. Quelque chose comme ceci:
\begin{minted}{python} \begin{minted}{python}
# models.py # models.py
@ -60,7 +58,7 @@ class Contract(models.Model):
date_end = models.DateField(blank=True, null=True) date_end = models.DateField(blank=True, null=True)
contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE) contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE)
service = models.ForeignKey(Service, on_delete=models.CASCADE) service = models.ForeignKey(Service, on_delete=models.CASCADE)
def __str__(self): def __str__(self):
if self.date_end is not None: if self.date_end is not None:
return "A partir du {}, jusqu'au {}, dans le service {} ({})".format( return "A partir du {}, jusqu'au {}, dans le service {} ({})".format(
@ -88,35 +86,31 @@ La configuration des points de terminaison de notre API est relativement
touffue. Il convient de: touffue. Il convient de:
\begin{enumerate} \begin{enumerate}
\item \item
Configurer les sérialiseurs, càd. les champs que nous souhaitons Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de l'API,
exposer au travers de l'API, \item
\item Configurer les vues, càd le comportement de chacun des points de terminaison,
Configurer les vues, càd le comportement de chacun des points de \item
terminaison, Configurer les points de terminaison eux-mêmes, càd les URLs permettant d'accéder aux ressources.
\item \item
Configurer les points de terminaison eux-mêmes, càd les URLs Et finalement ajouter quelques paramètres au niveau de notre application.
permettant d'accéder aux ressources.
\item
Et finalement ajouter quelques paramètres au niveau de notre
application.
\end{enumerate} \end{enumerate}
\subsection{Serialiseurs} \subsection{Serialiseurs}
\begin{minted}{python} \begin{minted}{python}
# serializers.py # serializers.py
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from rest_framework import serializers from rest_framework import serializers
from .models import People, Contract, Service from .models import People, Contract, Service
class PeopleSerializer(serializers.HyperlinkedModelSerializer): class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = People model = People
fields = ("last_name", "first_name", "contract_set") fields = ("last_name", "first_name", "contract_set")
class ContractSerializer(serializers.HyperlinkedModelSerializer): class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Contract model = Contract
@ -139,20 +133,20 @@ touffue. Il convient de:
from .models import People, Contract, Service from .models import People, Contract, Service
from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer
class PeopleViewSet(viewsets.ModelViewSet): class PeopleViewSet(viewsets.ModelViewSet):
queryset = People.objects.all() queryset = People.objects.all()
serializer_class = PeopleSerializer serializer_class = PeopleSerializer
permission_class = [permissions.IsAuthenticated] permission_class = [permissions.IsAuthenticated]
class ContractViewSet(viewsets.ModelViewSet): class ContractViewSet(viewsets.ModelViewSet):
queryset = Contract.objects.all() queryset = Contract.objects.all()
serializer_class = ContractSerializer serializer_class = ContractSerializer
permission_class = [permissions.IsAuthenticated] permission_class = [permissions.IsAuthenticated]
class ServiceViewSet(viewsets.ModelViewSet): class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all() queryset = Service.objects.all()
serializer_class = ServiceSerializer serializer_class = ServiceSerializer
@ -167,14 +161,14 @@ touffue. Il convient de:
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from rest_framework import routers from rest_framework import routers
from core import views from core import views
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r"people", views.PeopleViewSet) router.register(r"people", views.PeopleViewSet)
router.register(r"contracts", views.ContractViewSet) router.register(r"contracts", views.ContractViewSet)
router.register(r"services", views.ServiceViewSet) router.register(r"services", views.ServiceViewSet)
urlpatterns = [ urlpatterns = [
path("api/v1/", include(router.urls)), path("api/v1/", include(router.urls)),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@ -208,11 +202,11 @@ En nous rendant sur l'URL \texttt{http://localhost:8000/api/v1}, nous obtiendron
\section{Modéles et relations} \section{Modéles et relations}
Plus haut, nous avons utilisé une relation de type \texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité. Pour palier à ceci, il existe {[}plusieurs manières de représenter ces Plus haut, nous avons utilisé une relation de type \texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité. Pour palier à ceci, il existe {[}plusieurs manières de représenter ces
\url{https://www.django-rest-framework.org/api-guide/relations/}: \url{https://www.django-rest-framework.org/api-guide/relations/}:
\begin{enumerate} \begin{enumerate}
\item Soit \textbf{via} un hyperlien, comme ci-dessus, \item Soit \textbf{via} un hyperlien, comme ci-dessus,
\item Soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource. \item Soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource.
\end{enumerate} \end{enumerate}
La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats): La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats):

View File

@ -3,35 +3,34 @@
\chapter{Eléments d'architecture} \chapter{Eléments d'architecture}
\begin{quote} \begin{quote}
Un code mal pensé entraîne nécessairement une perte d'énergie et de temps. Un code mal pensé entraîne nécessairement une perte d'énergie et de temps.
Il est plus simple de réfléchir, au moment de la conception du programme, à une architecture permettant une meilleure maintenabilité que de devoir corriger un code "sale" \emph{a posteriori}. Il est plus simple de réfléchir, au moment de la conception du programme, à une architecture permettant une meilleure maintenabilité que de devoir corriger un code "sale" \emph{a posteriori}.
C'est pour aider les développeurs à rester dans le droit chemin que les principes SOLID ont été énumérés. \cite{gnu_linux_mag_hs_104} C'est pour aider les développeurs à rester dans le droit chemin que les principes SOLID ont été énumérés. \cite{gnu_linux_mag_hs_104}
\end{quote} \end{quote}
Les principes SOLID, introduit par Robert C. Martin dans les années 2000 pour orienter le développement de modules, sont les suivants: Les principes SOLID, introduit par Robert C. Martin dans les années 2000 pour orienter le développement de modules, sont les suivants:
\begin{enumerate} \begin{enumerate}
\item \item
\textbf{SRP} - Single responsibility principle - Principe de Responsabilité Unique \textbf{SRP} - Single responsibility principle - Principe de Responsabilité Unique
\item \item
\textbf{OCP} - Open-closed principle \textbf{OCP} - Open-closed principle
\item \item
\textbf{LSP} - Liskov Substitution \textbf{LSP} - Liskov Substitution
\item \item
\textbf{ISP} - Interface ségrégation principle \textbf{ISP} - Interface ségrégation principle
\item \item
\textbf{DIP} - Dependency Inversion Principle \textbf{DIP} - Dependency Inversion Principle
\end{enumerate} \end{enumerate}
Des équivalents à ces directives existent au niveau des composants, puis au niveau architectural: Des équivalents à ces directives existent au niveau des composants, puis au niveau architectural:
\begin{enumerate} \begin{enumerate}
\item \item
Reuse/release équivalence principle, Reuse/release équivalence principle,
\item \item
\textbf{CCP} - Common Closure Principle, \textbf{CCP} - Common Closure Principle,
\item \item
\textbf{CRP} - Common Reuse Principle. \textbf{CRP} - Common Reuse Principle.
\end{enumerate} \end{enumerate}
\includegraphics{images/arch-comp-modules.png} \includegraphics{images/arch-comp-modules.png}
@ -40,14 +39,12 @@ Des équivalents à ces directives existent au niveau des composants, puis au ni
\subsection{Single Responsility Principle} \label{SRP} \subsection{Single Responsility Principle} \label{SRP}
Le principe de responsabilité unique conseille de disposer de concepts ou domaines d'activité qui ne s'occupent chacun que d'une et une seule Le principe de responsabilité unique conseille de disposer de concepts ou domaines d'activité qui ne s'occupent chacun que d'une et une seule chose.
chose. Ceci rejoint (un peu) la \href{https://en.wikipedia.org/wiki/Unix_philosophy}{Philosophie Unix}, documentée par Doug McIlroy et qui demande de "\emph{faire une seule chose, mais de le faire bien}" \cite{unix_philosophy}.
Ceci rejoint (un peu) la \href{https://en.wikipedia.org/wiki/Unix_philosophy}{Philosophie Unix}, documentée par Doug McIlroy et qui demande de "\emph{faire une seule chose, mais de le faire bien}" \cite{unix_philosophy}.
Selon ce principe, une classe ou un élément de programmation ne doit donc pas avoir plus d'une seule raison de changer. Selon ce principe, une classe ou un élément de programmation ne doit donc pas avoir plus d'une seule raison de changer.
Plutôt que de centraliser le maximum de code à un seul endroit ou dans une seule classe par convenance ou commodité \footnote{Aussi appelé Plutôt que de centraliser le maximum de code à un seul endroit ou dans une seule classe par convenance ou commodité \footnote{Aussi appelé \emph{God-Like object}}, le principe de responsabilité unique suggère que chaque classe soit responsable d'un et un seul concept.
\emph{God-Like object}}, le principe de responsabilité unique suggère que chaque classe soit responsable d'un et un seul concept.
Une manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez disposer d'une classe représentant des données de membres du personnel; ces données pourraient être demandées par trois acteurs: Une manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez disposer d'une classe représentant des données de membres du personnel; ces données pourraient être demandées par trois acteurs:
@ -57,15 +54,15 @@ Une manière de voir les choses consiste à différencier les acteurs ou les int
\item Le COO (Chief Operating Officer) \item Le COO (Chief Operating Officer)
\end{enumerate} \end{enumerate}
Chacun d'entre eux aura besoin de données et d'informations relatives à ces membres du personnel, et provenant donc d'une même source de données centralisée. Chacun d'entre eux aura besoin de données et d'informations relatives à ces membres du personnel, et provenant donc d'une même source de données centralisée.
Mais chacun d'entre eux également besoin d'une représentation différente ou de traitements distincts. \cite{clean_architecture} Mais chacun d'entre eux également besoin d'une représentation différente ou de traitements distincts. \cite{clean_architecture}
Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux employés; celles-ci vont cependant un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de l'acteur concerné et de la manière dont il souhaite disposer des données. Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux employés; celles-ci vont cependant un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de l'acteur concerné et de la manière dont il souhaite disposer des données.
Dès que possible, identifiez les différents acteurs et demandeurs, en vue de prévoir les modifications qui pourraient être demandées par l'un d'entre eux. Dès que possible, identifiez les différents acteurs et demandeurs, en vue de prévoir les modifications qui pourraient être demandées par l'un d'entre eux.
Dans le cas d'un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres. Dans le cas d'un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres.
Vous trouverez ci-dessous une classe \texttt{Document}, dont chaque instance est représentée par trois propriétés: son titre, son contenu et sa date de publication. Vous trouverez ci-dessous une classe \texttt{Document}, dont chaque instance est représentée par trois propriétés: son titre, son contenu et sa date de publication.
Une méthode \texttt{render} permet également de proposer (très grossièrement) un type de sortie et un format de contenu: \texttt{XML} ou \texttt{Markdown}. Une méthode \texttt{render} permet également de proposer (très grossièrement) un type de sortie et un format de contenu: \texttt{XML} ou \texttt{Markdown}.
\begin{listing}[H] \begin{listing}[H]
@ -75,7 +72,7 @@ Une méthode \texttt{render} permet également de proposer (très grossièrement
self.title = title self.title = title
self.content = content self.content = content
self.published_at = published_at self.published_at = published_at
def render(self, format_type): def render(self, format_type):
if format_type == "XML": if format_type == "XML":
return """<?xml version = "1.0"?> return """<?xml version = "1.0"?>
@ -88,11 +85,11 @@ Une méthode \texttt{render} permet également de proposer (très grossièrement
self.content, self.content,
self.published_at.isoformat() self.published_at.isoformat()
) )
if format_type == "Markdown": if format_type == "Markdown":
import markdown import markdown
return markdown.markdown(self.content) return markdown.markdown(self.content)
raise ValueError( raise ValueError(
"Format type '{}' is not known".format(format_type) "Format type '{}' is not known".format(format_type)
) )
@ -132,21 +129,21 @@ En suivant le principe de responsabilité unique, une bonne pratique consiste à
self.content, self.content,
self.published_at.isoformat() self.published_at.isoformat()
) )
if format_type == "Markdown": if format_type == "Markdown":
import markdown import markdown
return markdown.markdown(self.content) return markdown.markdown(self.content)
raise ValueError("Format type '{}' is not known".format(format_type)) raise ValueError("Format type '{}' is not known".format(format_type))
\end{minted} \end{minted}
\caption{Isolation du rendu d'un document par rapport à sa modélisation} \caption{Isolation du rendu d'un document par rapport à sa modélisation}
\end{listing} \end{listing}
A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, il nous suffira de modifier la classe \texttt{DocumentRenderer}, sans que la classe \texttt{Document} ne soit impactée. A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, il nous suffira de modifier la classe \texttt{DocumentRenderer}, sans que la classe \texttt{Document} ne soit impactée.
En même temps, le jour où une instance de type \texttt{Document} sera liée à un champ \texttt{author}, rien ne dit que le rendu devra en tenir compte; nous modifierons donc notre classe pour y ajouter le nouveau champ sans que cela n'impacte nos différentes manières d'effectuer un rendu. En même temps, le jour où une instance de type \texttt{Document} sera liée à un champ \texttt{author}, rien ne dit que le rendu devra en tenir compte; nous modifierons donc notre classe pour y ajouter le nouveau champ sans que cela n'impacte nos différentes manières d'effectuer un rendu.
Un autre exemple consiterait à faire communiquer une méthode avec une base de données: ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement spécifique (emplacement sur un disque, ...): cette action doit être prise en compte par une autre classe (ou un autre concept ou composant), qui s'occupera de définir elle-même l'emplacement où l'évènement sera enregistré, que ce soit dans une base de données, une instance Graylog ou un fichier. Un autre exemple consiterait à faire communiquer une méthode avec une base de données: ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement spécifique (emplacement sur un disque, ...): cette action doit être prise en compte par une autre classe (ou un autre concept ou composant), qui s'occupera de définir elle-même l'emplacement où l'évènement sera enregistré, que ce soit dans une base de données, une instance Graylog ou un fichier.
Cette manière de structurer le code permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce qui augmente ainsi la testabilité globale du projet. Cette manière de structurer le code permet de centraliser la configuration d'un type d'évènement à un seul endroit, ce qui augmente ainsi la testabilité globale du projet.
L'équivalent du principe de responsabilité unique au niveau des composants sera le \texttt{Common Closure Principle} \index{CCP}. L'équivalent du principe de responsabilité unique au niveau des composants sera le \texttt{Common Closure Principle} \index{CCP}.
@ -157,21 +154,21 @@ Au niveau architectural, cet équivalent correspondra aux frontières.
\begin{quote} \begin{quote}
For software systems to be easy to change, they must be designed to allow the behavior to change by adding new code instead of changing existing code. For software systems to be easy to change, they must be designed to allow the behavior to change by adding new code instead of changing existing code.
\end{quote} \end{quote}
L'objectif est de rendre le système facile à étendre, en limitant l'impact qu'une modification puisse avoir. L'objectif est de rendre le système facile à étendre, en limitant l'impact qu'une modification puisse avoir.
Reprendre notre exemple de modélisation de documents parle de lui-même: Reprendre notre exemple de modélisation de documents parle de lui-même:
\begin{enumerate} \begin{enumerate}
\item Des données que nous avons converties dans un format spécifique pourraient à présent devoir être présentées dans une page web. \item Des données que nous avons converties dans un format spécifique pourraient à présent devoir être présentées dans une page web.
\item Et demain, ce sera dans un document PDF. \item Et demain, ce sera dans un document PDF.
\item Et après demain, dans un tableur Excel. \item Et après demain, dans un tableur Excel.
\end{enumerate} \end{enumerate}
La source de ces données reste la même (au travers d'une couche de présentation): c'est leur mise en forme qui diffère à chaque fois. La source de ces données reste la même (au travers d'une couche de présentation): c'est leur mise en forme qui diffère à chaque fois.
L'application n'a pas à connaître les détails d'implémentation: elle doit juste permettre une forme d'extension, sans avoir à appliquer quelconque modification en son cœur. L'application n'a pas à connaître les détails d'implémentation: elle doit juste permettre une forme d'extension, sans avoir à appliquer quelconque modification en son cœur.
Un des principes essentiels en programmation orientée objets concerne l'héritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons comme nous l'avons initisée plus tôt pour définir le comportement d'une instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise. Un des principes essentiels en programmation orientée objets concerne l'héritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons comme nous l'avons initisée plus tôt pour définir le comportement d'une instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise.
Pour prendre un nouvel exemple, nous pourrions ainsi définir trois classes: Pour prendre un nouvel exemple, nous pourrions ainsi définir trois classes:
\begin{itemize} \begin{itemize}
@ -183,7 +180,7 @@ Pour prendre un nouvel exemple, nous pourrions ainsi définir trois classes:
Une classe \texttt{GoldCustomer}, pour laquelle la même méthode renvoit une réduction de 20\%. Une classe \texttt{GoldCustomer}, pour laquelle la même méthode renvoit une réduction de 20\%.
\end{itemize} \end{itemize}
Si nous devions rencontrer un nouveau type de client, il nous suffira de créer une nouvelle sous-classe, implémentant la réduction que nous souhaitons lui offrir. Si nous devions rencontrer un nouveau type de client, il nous suffira de créer une nouvelle sous-classe, implémentant la réduction que nous souhaitons lui offrir.
Ceci évite d'avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d'une variable ou d'un paramètre - ici, le type de client. Ceci évite d'avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d'une variable ou d'un paramètre - ici, le type de client.
Nous passerions ainsi de ceci: Nous passerions ainsi de ceci:
@ -193,7 +190,7 @@ Nous passerions ainsi de ceci:
class Customer(): class Customer():
def __init__(self, customer_type: str): def __init__(self, customer_type: str):
self.customer_type = customer_type self.customer_type = customer_type
def get_discount(customer: Customer) -> int: def get_discount(customer: Customer) -> int:
if customer.customer_type == "Silver": if customer.customer_type == "Silver":
return 10 return 10
@ -214,15 +211,15 @@ A ceci:
class Customer(): class Customer():
def get_discount(self) -> int: def get_discount(self) -> int:
return 0 return 0
class SilverCustomer(Customer): class SilverCustomer(Customer):
def get_discount(self) -> int: def get_discount(self) -> int:
return 10 return 10
class GoldCustomer(Customer): class GoldCustomer(Customer):
def get_discount(self) -> int: def get_discount(self) -> int:
return 20 return 20
>>> jack = SilverCustomer() >>> jack = SilverCustomer()
>>> jack.get_discount() >>> jack.get_discount()
10 10
@ -244,7 +241,7 @@ Nous pouvons également appliquer ceci à notre exemple sur les rendus de docume
self.title = title self.title = title
self.content = content self.content = content
self.published_at = published_at self.published_at = published_at
def render(self, format_type): def render(self, format_type):
if format_type == "XML": if format_type == "XML":
return """<?xml version = "1.0"?> return """<?xml version = "1.0"?>
@ -257,11 +254,11 @@ Nous pouvons également appliquer ceci à notre exemple sur les rendus de docume
self.content, self.content,
self.published_at.isoformat() self.published_at.isoformat()
) )
if format_type == "Markdown": if format_type == "Markdown":
import markdown import markdown
return markdown.markdown(self.content) return markdown.markdown(self.content)
raise ValueError( raise ValueError(
"Format type '{}' is not known".format(format_type) "Format type '{}' is not known".format(format_type)
) )
@ -275,7 +272,7 @@ devient le suivant:
class Renderer: class Renderer:
def render(self, document): def render(self, document):
raise NotImplementedError raise NotImplementedError
class XmlRenderer(Renderer): class XmlRenderer(Renderer):
def render(self, document) def render(self, document)
return """<?xml version = "1.0"?> return """<?xml version = "1.0"?>
@ -288,7 +285,7 @@ devient le suivant:
document.content, document.content,
document.published_at.isoformat() document.published_at.isoformat()
) )
class MarkdownRenderer(Renderer): class MarkdownRenderer(Renderer):
def render(self, document): def render(self, document):
import markdown import markdown
@ -301,39 +298,39 @@ Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons simplement
\subsection{Liskov Substitution} \subsection{Liskov Substitution}
Le principe de substitution fait qu'une classe héritant d'une autre classe doit se comporter de la même manière que cette dernière. Le principe de substitution fait qu'une classe héritant d'une autre classe doit se comporter de la même manière que cette dernière.
Il n'est pas question que la sous-classe n'implémente pas ou n'ait pas besoin de certaines méthodes, alors que celles-ci sont disponibles sa classe parente. Il n'est pas question que la sous-classe n'implémente pas ou n'ait pas besoin de certaines méthodes, alors que celles-ci sont disponibles sa classe parente.
Mathématiquement, ce principe peut être défini de la manière suivante: Mathématiquement, ce principe peut être défini de la manière suivante:
\begin{quote} \begin{quote}
{[}\ldots\hspace{0pt}{]} if S is a subtype of T, then objects of type T in a computer program may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T), without altering any of the desirable properties of that program (correctness, task performed, etc.). {[}\ldots\hspace{0pt}{]} if S is a subtype of T, then objects of type T in a computer program may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T), without altering any of the desirable properties of that program (correctness, task performed, etc.).
--- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia}. --- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia}.
\end{quote} \end{quote}
\begin{quote} \begin{quote}
Let q(x) be a property provable about objects x of type T. Let q(x) be a property provable about objects x of type T.
Then q(y) should be provable for objects y of type S, where S is a subtype of T. Then q(y) should be provable for objects y of type S, where S is a subtype of T.
--- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia aussi} --- \href{http://en.wikipedia.org/wiki/Liskov_substitution_principle}{Wikipédia aussi}
\end{quote} \end{quote}
Ce n'est donc pas parce qu'une classe \textbf{a besoin d'une méthode définie dans une autre classe} qu'elle doit forcément en hériter. Ce n'est donc pas parce qu'une classe \textbf{a besoin d'une méthode définie dans une autre classe} qu'elle doit forcément en hériter.
Cela bousillerait le principe de substitution, dans la mesure où une instance de cette classe pourra toujours être considérée comme étant du type de son parent. Cela bousillerait le principe de substitution, dans la mesure où une instance de cette classe pourra toujours être considérée comme étant du type de son parent.
Petit exemple pratique: si nous définissons une méthode \texttt{make\_some\_noise} et une méthode \texttt{eat} sur une classe \texttt{Duck}, et qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "\emph{Puisqu'un \texttt{Lion} fait aussi du bruit, faisons le hériter de notre classe `Canard`"}, nous allons nous retrouver avec ceci: Petit exemple pratique: si nous définissons une méthode \texttt{make\_some\_noise} et une méthode \texttt{eat} sur une classe \texttt{Duck}, et qu'une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "\emph{Puisqu'un \texttt{Lion} fait aussi du bruit, faisons le hériter de notre classe `Canard`"}, nous allons nous retrouver avec ceci:
\begin{listing}[H] \begin{listing}[H]
\begin{minted}[tabsize=4]{Python} \begin{minted}[tabsize=4]{Python}
class Duck: class Duck:
def make_some_noise(self): def make_some_noise(self):
print("Kwak") print("Kwak")
def eat(self, thing): def eat(self, thing):
if thing in ("plant", "insect", "seed", "seaweed", "fish"): if thing in ("plant", "insect", "seed", "seaweed", "fish"):
return "Yummy!" return "Yummy!"
raise IndigestionError("Arrrh") raise IndigestionError("Arrrh")
class Lion(Duck): class Lion(Duck):
def make_some_noise(self): def make_some_noise(self):
print("Roaaar!") print("Roaaar!")
@ -341,8 +338,9 @@ Petit exemple pratique: si nous définissons une méthode \texttt{make\_some\_no
\caption{Un lion et un canard sont sur un bateau...} \caption{Un lion et un canard sont sur un bateau...}
\end{listing} \end{listing}
Le principe de substitution de Liskov suggère qu'une classe doit toujours pouvoir être considérée comme une instance de sa classe parente, et \textbf{doit pouvoir s'y substituer}. Le principe de substitution de Liskov suggère qu'une classe doit toujours pouvoir être considérée comme une instance de sa classe parente, et \textbf{doit pouvoir s'y substituer}.
Dans notre exemple, cela signifie que nous pourrons tout à fait accepter qu'un lion se comporte comme un canard et adore manger des plantes, insectes, graines, algues et du poisson. Miam ! Dans notre exemple, cela signifie que nous pourrons tout à fait accepter qu'un lion se comporte comme un canard et adore manger des plantes, insectes, graines, algues et du poisson.
Miam !
Nous vous laissons tester la structure ci-dessus en glissant une antilope dans la boite à goûter du lion, ce qui nous donnera quelques trucs bizarres (et un lion atteint de botulisme). Nous vous laissons tester la structure ci-dessus en glissant une antilope dans la boite à goûter du lion, ce qui nous donnera quelques trucs bizarres (et un lion atteint de botulisme).
Pour revenir à nos exemples de rendus de documents, nous aurions pu faire hériter notre \texttt{MarkdownRenderer} de la classe \texttt{XmlRenderer}: Pour revenir à nos exemples de rendus de documents, nous aurions pu faire hériter notre \texttt{MarkdownRenderer} de la classe \texttt{XmlRenderer}:
@ -361,7 +359,7 @@ Pour revenir à nos exemples de rendus de documents, nous aurions pu faire héri
document.content, document.content,
document.published_at.isoformat() document.published_at.isoformat()
) )
class MarkdownRenderer(XmlRenderer): class MarkdownRenderer(XmlRenderer):
def render(self, document): def render(self, document):
import markdown import markdown
@ -390,13 +388,13 @@ Si nous décidons à un moment d'ajouter une méthode d'entête au niveau de not
document.content, document.content,
document.published_at.isoformat() document.published_at.isoformat()
) )
class MarkdownRenderer(XmlRenderer): class MarkdownRenderer(XmlRenderer):
def render(self, document): def render(self, document):
import markdown import markdown
return markdown.markdown(document.content) return markdown.markdown(document.content)
\end{minted} \end{minted}
\caption{... et il a mal à l'entête} \caption{\ldots~ et il a mal à l'entête}
\end{listing} \end{listing}
Le code ci-dessus ne porte pas à conséquence \footnote{Pas immédiatement, en tout cas...}, mais dès que nous invoquerons la méthode \texttt{header()} sur une instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de déclaration XML (\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un fichier Markdown, ce qui n'aura aucun sens. Le code ci-dessus ne porte pas à conséquence \footnote{Pas immédiatement, en tout cas...}, mais dès que nous invoquerons la méthode \texttt{header()} sur une instance de type \texttt{MarkdownRenderer}, nous obtiendrons un bloc de déclaration XML (\texttt{\textless{}?xml\ version\ =\ "1.0"?\textgreater{}}) pour un fichier Markdown, ce qui n'aura aucun sens.
@ -425,7 +423,7 @@ En revenant à notre proposition d'implémentation, suite au respect d'Open-Clos
document.content, document.content,
document.published_at.isoformat() document.published_at.isoformat()
) )
class MarkdownRenderer(Renderer): class MarkdownRenderer(Renderer):
def render(self, document): def render(self, document):
import markdown import markdown
@ -436,15 +434,15 @@ En revenant à notre proposition d'implémentation, suite au respect d'Open-Clos
\subsection{Interface Segregation} \subsection{Interface Segregation}
Le principe de ségrégation d'interface suggère de n'exposer que les opérations nécessaires à l'exécution d'un contexte. Le principe de ségrégation d'interface suggère de n'exposer que les opérations nécessaires à l'exécution d'un contexte.
Ceci limite la nécessité de recompiler un module, et évite ainsi d'avoir à redéployer l'ensemble d'une application alors qu'il suffirait de déployer un nouveau fichier JAR ou une DLL au bon endroit. Ceci limite la nécessité de recompiler un module, et évite ainsi d'avoir à redéployer l'ensemble d'une application alors qu'il suffirait de déployer un nouveau fichier JAR ou une DLL au bon endroit.
\begin{quote} \begin{quote}
The lesson here is that depending on something that carries baggage that you don't need can cause you troubles that you didn't except. The lesson here is that depending on something that carries baggage that you don't need can cause you troubles that you didn't except.
\end{quote} \end{quote}
Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces. Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces.
Ceci permet aux différents consommateurs de n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à un besoin précis, et permet donc à nos clients de ne pas dépendre de méthodes dont ils n'ont pas besoin. Ceci permet aux différents consommateurs de n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à un besoin précis, et permet donc à nos clients de ne pas dépendre de méthodes dont ils n'ont pas besoin.
GNU/Linux Magazine \cite[pp. 37-42]{gnu_linux_mag_hs_104} propose un exemple d'interface permettant d'implémenter une imprimante: GNU/Linux Magazine \cite[pp. 37-42]{gnu_linux_mag_hs_104} propose un exemple d'interface permettant d'implémenter une imprimante:
@ -456,11 +454,11 @@ GNU/Linux Magazine \cite[pp. 37-42]{gnu_linux_mag_hs_104} propose un exemple d'i
public abstract void scanPage(); public abstract void scanPage();
public abstract void faxPage(); public abstract void faxPage();
} }
public class Printer public class Printer
{ {
protected string name; protected string name;
public Printer(string name) public Printer(string name)
{ {
this.name = name; this.name = name;
@ -480,17 +478,17 @@ Limplémentation dune imprimante multifonction aura tout son sens:
{ {
super(name); super(name);
} }
public void printPage() public void printPage()
{ {
System.out.println(this.name + ": Impression"); System.out.println(this.name + ": Impression");
} }
public void scanPage() public void scanPage()
{ {
System.out.println(this.name + ": Scan"); System.out.println(this.name + ": Scan");
} }
public void faxPage() public void faxPage()
{ {
System.out.println(this.name + ": Fax"); System.out.println(this.name + ": Fax");
@ -510,17 +508,17 @@ Tandis que limplémentation dune imprimante premier-prix ne servira pas à
{ {
super(name); super(name);
} }
public void printPage() public void printPage()
{ {
System.out.println(this.name + ": Impression"); System.out.println(this.name + ": Impression");
} }
public void scanPage() public void scanPage()
{ {
System.out.println(this.name + ": Fonctionnalité absente"); System.out.println(this.name + ": Fonctionnalité absente");
} }
public void faxPage() public void faxPage()
{ {
System.out.println(this.name + ": Fonctionnalité absente"); System.out.println(this.name + ": Fonctionnalité absente");
@ -553,7 +551,7 @@ Lobjectif est donc de découpler ces différentes fonctionnalités en plusieu
\end{listing} \end{listing}
Cette réflexion s'applique à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, les variables déclarées, ... Cette réflexion s'applique à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, les variables déclarées, ...
Quel que soit le composant que l'on utilise ou analyse, il est plus qu'intéressant de se limiter uniquement à ce dont nous avons besoin plutôt que d'embarquer le must absolu qui peut faire 1000x fonctions de plus que n'importe quel autre produit, alors que seules deux d'entre elles seront nécessaires. Quel que soit le composant que l'on utilise ou analyse, il est plus qu'intéressant de se limiter uniquement à ce dont nous avons besoin plutôt que d'embarquer le must absolu qui peut faire 1000x fonctions de plus que n'importe quel autre produit, alors que seules deux d'entre elles seront nécessaires.
En Python, ce comportement est inféré lors de l'exécution, et donc pas vraiment d'application pour ce contexte d'étude: de manière plus générale, les langages dynamiques sont plus flexibles et moins couplés que les langages statiquement typés, pour lesquels l'application de ce principe-ci permettrait de juste mettre à jour une DLL ou un JAR sans que cela n'ait d'impact sur le reste de l'application. En Python, ce comportement est inféré lors de l'exécution, et donc pas vraiment d'application pour ce contexte d'étude: de manière plus générale, les langages dynamiques sont plus flexibles et moins couplés que les langages statiquement typés, pour lesquels l'application de ce principe-ci permettrait de juste mettre à jour une DLL ou un JAR sans que cela n'ait d'impact sur le reste de l'application.
@ -569,25 +567,25 @@ Il est ainsi possible de trouver quelques horreurs, et ce dans tous les langages
* Released under the MIT License. * Released under the MIT License.
*/ */
'use strict'; 'use strict';
const isNumber = require('is-number'); const isNumber = require('is-number');
module.exports = function isOdd(value) { module.exports = function isOdd(value) {
const n = Math.abs(value); const n = Math.abs(value);
if (!isNumber(n)) { if (!isNumber(n)) {
throw new TypeError('expected a number'); throw new TypeError('expected a number');
} }
if (!Number.isInteger(n)) { if (!Number.isInteger(n)) {
throw new Error('expected an integer'); throw new Error('expected an integer');
} }
if (!Number.isSafeInteger(n)) { if (!Number.isSafeInteger(n)) {
throw new Error('value exceeds maximum safe integer'); throw new Error('value exceeds maximum safe integer');
} }
return (n % 2) === 1; return (n % 2) === 1;
} }
\end{minted} \end{minted}
@ -615,21 +613,21 @@ Voire, son opposé, qui dépend évidemment du premier:
\caption{Le module 'isEven', en JavaScript, qui dépend du premier} \caption{Le module 'isEven', en JavaScript, qui dépend du premier}
\end{listing} \end{listing}
Il ne s'agit que d'un simple exemple, mais qui tend à une seule chose: gardez les choses simples (et, éventuellement, stupides). \index{KISS} Il ne s'agit que d'un simple exemple, mais qui tend à une seule chose: gardez les choses simples (et, éventuellement, stupides). \index{KISS}
Dans l'exemple ci-dessus, l'utilisation du module \texttt{is-odd} requière déjà deux dépendances: Dans l'exemple ci-dessus, l'utilisation du module \texttt{is-odd} requière déjà deux dépendances:
\begin{enumerate} \begin{enumerate}
\item \texttt{is-even} \item \texttt{is-even}
\item \texttt{is-number} \item \texttt{is-number}
\end{enumerate} \end{enumerate}
Imaginez la suite. Imaginez la suite.
\subsection{Dependency Inversion} \subsection{Dependency Inversion}
Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau. Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau.
L'inversion de dépendances stipule que c'est le composant de haut-niveau qui possède la définition de l'interface dont il a besoin, et le composant de bas-niveau qui l'implémente. L'inversion de dépendances stipule que c'est le composant de haut-niveau qui possède la définition de l'interface dont il a besoin, et le composant de bas-niveau qui l'implémente.
L'objectif est que les interfaces soient les plus stables possibles, afin de réduire au maximum les modifications qui pourraient y être appliquées. L'objectif est que les interfaces soient les plus stables possibles, afin de réduire au maximum les modifications qui pourraient y être appliquées.
De cette manière, toute modification fonctionnelle pourra être directement appliquée sur le composant de bas-niveau, sans que l'interface ne soit impactée. De cette manière, toute modification fonctionnelle pourra être directement appliquée sur le composant de bas-niveau, sans que l'interface ne soit impactée.
\begin{quote} \begin{quote}
@ -639,7 +637,7 @@ abstractions, not to concretions. \cite{clean_architecture}
L'injection de dépendances est un patron de programmation qui suit le principe d'inversion de dépendances. L'injection de dépendances est un patron de programmation qui suit le principe d'inversion de dépendances.
Django est bourré de ce principe, que ce soit pour les \emph{middlewares} ou pour les connexions aux bases de données. Django est bourré de ce principe, que ce soit pour les \emph{middlewares} ou pour les connexions aux bases de données.
Lorsque nous écrivons ceci dans notre fichier de configuration, Lorsque nous écrivons ceci dans notre fichier de configuration,
\begin{listing}[H] \begin{listing}[H]
@ -660,10 +658,10 @@ Lorsque nous écrivons ceci dans notre fichier de configuration,
\end{listing} \end{listing}
Django ira simplement récupérer chacun de ces middlewares, qui répondent chacun à une Django ira simplement récupérer chacun de ces middlewares, qui répondent chacun à une
\href{https://docs.djangoproject.com/en/4.0/topics/http/middleware/\#writing-your-own-middleware}{interface clairement définie}, dans l'ordre. \href{https://docs.djangoproject.com/en/4.0/topics/http/middleware/\#writing-your-own-middleware}{interface clairement définie}, dans l'ordre.
Il n'y a donc pas de magie: l'interface exige une signature particulière, tandis que l'implémentation effective n'est réalisée qu'au niveau le plus bas. Il n'y a donc pas de magie: l'interface exige une signature particulière, tandis que l'implémentation effective n'est réalisée qu'au niveau le plus bas.
C'est ensuite le développeur qui va simplement brancher ou câbler des fonctionnalités au niveau du framework, en les déclarant au bon endroit. C'est ensuite le développeur qui va simplement brancher ou câbler des fonctionnalités au niveau du framework, en les déclarant au bon endroit.
Pour créer un nouveau \emph{middleware}, il nous suffirait d'implémenter de nouvelles fonctionnalités au niveau du code code suivant et de l'ajouter dans la configuration de l'application, au niveau de la liste des middlewares actifs: Pour créer un nouveau \emph{middleware}, il nous suffirait d'implémenter de nouvelles fonctionnalités au niveau du code code suivant et de l'ajouter dans la configuration de l'application, au niveau de la liste des middlewares actifs:
\begin{listing}[H] \begin{listing}[H]
@ -676,10 +674,10 @@ Pour créer un nouveau \emph{middleware}, il nous suffirait d'implémenter de no
# the view (and later middleware) are called. # the view (and later middleware) are called.
response = get_response(request) response = get_response(request)
# Code to be executed for each request/response after # Code to be executed for each request/response after
# the view is called. # the view is called.
return response return response
return middleware return middleware
@ -690,14 +688,12 @@ Pour créer un nouveau \emph{middleware}, il nous suffirait d'implémenter de no
Dans d'autres projets écrits en Python, ce type de mécanisme peut être implémenté relativement facilement en utilisant les modules \href{https://docs.python.org/3/library/importlib.html}{importlib} et la fonction \texttt{getattr}. Dans d'autres projets écrits en Python, ce type de mécanisme peut être implémenté relativement facilement en utilisant les modules \href{https://docs.python.org/3/library/importlib.html}{importlib} et la fonction \texttt{getattr}.
Un autre exemple concerne les bases de données: pour garder un maximum de flexibilité, Django ajoute une couche d'abstraction en permettant de Un autre exemple concerne les bases de données: pour garder un maximum de flexibilité, Django ajoute une couche d'abstraction en permettant de spécifier le moteur de base de données que vous souhaiteriez utiliser, qu'il s'agisse d'SQLite, MSSQL, Oracle, PostgreSQL ou MySQL/MariaDB \footnote{\url{http://howfuckedismydatabase.com/}}.
spécifier le moteur de base de données que vous souhaiteriez utiliser, qu'il s'agisse d'SQLite, MSSQL, Oracle, PostgreSQL ou MySQL/MariaDB \footnote{\url{http://howfuckedismydatabase.com/}}.
D'un point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, s'il s'agit d'un disque magnétique, de mémoire vive, ... en fait, on ne devrait même pas savoir s'il y a un disque du tout. D'un point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, s'il s'agit d'un disque magnétique, de mémoire vive, \ldots~ en fait, on ne devrait même pas savoir s'il y a un disque du tout.
Et Django le fait très bien pour nous. Et Django le fait très bien pour nous.
En termes architecturaux, ce principe autorise une définition des frontières, et en permettant une séparation claire en inversant le flux En termes architecturaux, ce principe autorise une définition des frontières, et en permettant une séparation claire en inversant le flux de dépendances et en faisant en sorte que les règles métiers n'aient aucune connaissance des interfaces graphiques qui les exploitent ou desmoteurs de bases de données qui les stockent.
de dépendances et en faisant en sorte que les règles métiers n'aient aucune connaissance des interfaces graphiques qui les exploitent ou desmoteurs de bases de données qui les stockent.
Ceci autorise une forme d'immunité entre les composants. Ceci autorise une forme d'immunité entre les composants.
\section{Composants} \section{Composants}
@ -717,7 +713,7 @@ Ceci autorise une forme d'immunité entre les composants.
Plus spécifiquement, la définition exacte devient celle-ci: Plus spécifiquement, la définition exacte devient celle-ci:
\begin{quote} \begin{quote}
Gather together those things that change at the same times and for the same reasons. Gather together those things that change at the same times and for the same reasons.
Separate those things that change at different times or for different reasons. Separate those things that change at different times or for different reasons.
\end{quote} \end{quote}
@ -725,48 +721,48 @@ Que l'on résumera ainsi: "dont depend on things you dont need", comme nou
\subsection{Stable Dependency Principle} \label{SDP} \subsection{Stable Dependency Principle} \label{SDP}
Ce principe définit une formule de stabilité pour les composants, en fonction de leur faculté à être modifié et des composants qui dépendent de lui: au plus un composant est nécessaire, au plus il sera stable (dans la mesure où il lui sera difficile de changer). Ce principe définit une formule de stabilité pour les composants, en fonction de leur faculté à être modifié et des composants qui dépendent de lui : au plus un composant est nécessaire, au plus il sera stable (dans la mesure où il lui sera difficile de changer).
En C++, cela correspond aux mots clés \#include. En C++, cela correspond aux mots clés \#include.
Pour faciliter cette stabilité, il convient de passer par des interfaces (donc, rarement modifiées, par définition). Pour faciliter cette stabilité, il convient de passer par des interfaces (donc, rarement modifiées, par définition).
En Python, ce ratio pourrait être calculé au travers des import, via les AST. En Python, ce ratio pourrait être calculé au travers des import, via les AST.
\subsection{Stable Abstraction Principle} \label{SAP} \subsection{Stable Abstraction Principle} \label{SAP}
Ce principe-ci définit les politiques de haut niveau vs les composants plus concrets. Ce principe-ci définit les politiques de haut niveau vs les composants plus concrets.
SAP est juste une modélisation du OCP pour les composants: nous plaçons ceux qui ne changent pas ou pratiquement pas le plus haut possible dans l'organigramme (ou le diagramme), et ceux qui changent souvent plus bas, dans le sens de stabilité du flux. SAP est juste une modélisation du OCP pour les composants : nous plaçons ceux qui ne changent pas ou pratiquement pas le plus haut possible dans l'organigramme (ou le diagramme), et ceux qui changent souvent plus bas, dans le sens de stabilité du flux.
Les composants les plus bas sont considérés comme volatiles. Les composants les plus bas sont considérés comme volatiles.
\section{Architecture générale} \section{Architecture générale}
\begin{quote} \begin{quote}
If you think good architecture is expensive, try bad architecture If you think good architecture is expensive, try bad architecture
--- Brian Foote \& Joseph Yoder --- Brian Foote \& Joseph Yoder
\end{quote} \end{quote}
La flexiiblité de l'architecture générale est moins permissive que celle allouée aux modules ou aux composants, dans la mesure où elle définit en partie les frontières et interactions possibles avec le monde extérieur. La flexiiblité de l'architecture générale est moins permissive que celle allouée aux modules ou aux composants, dans la mesure où elle définit en partie les frontières et interactions possibles avec le monde extérieur.
Il est nécessaire de projeter la capacité d'adaptation, en minimisant la maintenance. Il est nécessaire de projeter la capacité d'adaptation, en minimisant la maintenance.
Un des problèmes est qu'à la première demande, l'architecture pourrait avoir pris une mauvaise direction, sans aucune malléabilité. Un des problèmes est qu'à la première demande, l'architecture pourrait avoir pris une mauvaise direction, sans aucune malléabilité.
Une bonne architecture va rendre le système facile à lire, facile à développer, facile à maintenir et facile à déployer, l'objectif ultime étant de minimiser le coût de maintenance et de maximiser la productivité des développeurs. Une bonne architecture va rendre le système facile à lire, facile à développer, facile à maintenir et facile à déployer, l'objectif ultime étant de minimiser le coût de maintenance et de maximiser la productivité des développeurs.
Un des autres objectifs d'une bonne architecture consiste à se garder le plus d'options possibles, et à se concentrer sur les détails (le type de base de données, la conception concrète, ...) le plus tard possible, tout en conservant la politique principale en ligne de mire. Un des autres objectifs d'une bonne architecture consiste à se garder le plus d'options possibles, et à se concentrer sur les détails (le type de base de données, la conception concrète, \ldots) le plus tard possible, tout en conservant la politique principale en ligne de mire.
Ceci permet de délayer les choix techniques à «~plus tard~», ce qui permet également de concrétiser ces choix en ayant le plus d'informations possibles \cite[pp.137-141]{clean_architecture} Ceci permet de délayer les choix techniques à «~plus tard~», ce qui permet également de concrétiser ces choix en ayant le plus d'informations possibles \cite[pp.137-141]{clean_architecture}
Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires à faire évoluer l'application: ne Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires à faire évoluer l'application: ne
pas investir dès qu'on le peut va juste lentement remplir la case de la dette technique. pas investir dès qu'on le peut va juste lentement remplir la case de la dette technique.
Une architecture ouverte et pouvant être étendue n'a d'intérêt que si le développement est suivi et que les gestionnaires (et architectes) s'engagent à économiser du temps et de la qualité lorsque des changements seront demandés pour l'évolution du projet: ne pas investir à améliorer l'architecture dès que ce sera possible fera lentement (mais sûrement!) dériver la base de code vers une augmentation de la dette technique. Une architecture ouverte et pouvant être étendue n'a d'intérêt que si le développement est suivi et que les gestionnaires (et architectes) s'engagent à économiser du temps et de la qualité lorsque des changements seront demandés pour l'évolution du projet : ne pas investir à améliorer l'architecture dès que ce sera possible fera lentement (mais sûrement!) dériver la base de code vers une augmentation de la dette technique.
Faire évoluer correctement l'architecture d'un projet demande une bonne expérience, mais également un bon sens de l'observation, un investissement non négligeable en attention portée aux détails et de la patience: Faire évoluer correctement l'architecture d'un projet demande une bonne expérience, mais également un bon sens de l'observation, un investissement non négligeable en attention portée aux détails et de la patience:
\begin{quote} \begin{quote}
This is not a one time decision. This is not a one time decision.
You don't simply decide at the start of a project which boundaries to implement and which to ignore. You don't simply decide at the start of a project which boundaries to implement and which to ignore.
Rather, you watch. Rather, you watch.
You pay attention as the system evolves. You pay attention as the system evolves.
You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don't exist. You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don't exist.
At that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring. At that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring.
\end{quote} \end{quote}
@ -775,104 +771,83 @@ Faire évoluer correctement l'architecture d'un projet demande une bonne expéri
\section{Considérations sur les frameworks} \section{Considérations sur les frameworks}
\begin{quote} \begin{quote}
Frameworks are tools to be used, not architectures to be conformed to. Frameworks are tools to be used, not architectures to be conformed to.
Your architecture should tell readers about the system, not about the Your architecture should tell readers about the system, not about the
frameworks you used in your system. If you are building a health care frameworks you used in your system. If you are building a health care
system, then when new programmers look at the source repository, their system, then when new programmers look at the source repository, their
first impression should be, «~oh, this is a health care system~». Those first impression should be, «~oh, this is a health care system~». Those
new programmers should be able to learn all the use cases of the system, new programmers should be able to learn all the use cases of the system,
yet still not know how the system is delivered. yet still not know how the system is delivered.
--- Robert C. Martin Clean Architecture --- Robert C. Martin Clean Architecture
\end{quote} \end{quote}
Le point soulevé ci-dessous est qu'un framework n'est qu'un outil, et Le point soulevé ci-dessous est qu'un framework n'est qu'un outil, et pas une obligation de structuration.
pas une obligation de structuration. L'idée est que le framework doit se L'idée est que le framework doit se conformer à la définition de l'application, et non l'inverse.
conformer à la définition de l'application, et non l'inverse. Dans le Dans le cadre de l'utilisation de Django, c'est un point critique à prendre en considération: une fois que vous aurez fait ce choix, vous aurez extrêmement difficile à faire machine arrière:
cadre de l'utilisation de Django, c'est un point critique à prendre en
considération: une fois que vous aurez fait ce choix, vous aurez
extrêmement difficile à faire machine arrière:
\begin{itemize} \begin{itemize}
\item \item
Votre modèle métier sera largement couplé avec le type de base de Votre modèle métier sera largement couplé avec le type de base de données (relationnelle, indépendamment
données (relationnelle, indépendamment \item
\item Votre couche de présentation sera surtout disponible au travers d'un navigateur
Votre couche de présentation sera surtout disponible au travers d'un \item
navigateur Les droits d'accès et permissions seront en grosse partie gérés par le frameworks
\item \item
Les droits d'accès et permissions seront en grosse partie gérés par le
frameworks
\item
La sécurité dépendra de votre habilité à suivre les versions La sécurité dépendra de votre habilité à suivre les versions
\item \item
Et les fonctionnalités complémentaires (que vous n'aurez pas voulu/eu Et les fonctionnalités complémentaires (que vous n'aurez pas voulu/eu le temps de développer) dépendront de la bonne volonté de la communauté
le temps de développer) dépendront de la bonne volonté de la
communauté
\end{itemize} \end{itemize}
Le point à comprendre ici n'est pas que "Django, c'est mal", mais qu'une Le point à comprendre ici n'est pas que "Django, c'est mal", mais qu'une fois que vous aurez défini la politique, les règles métiers, les données critiques et entités, et que vous aurez fait le choix de développer en âme et conscience votre nouvelle création en utilisant Django, vous serez bon gré mal gré, contraint de continuer avec.
fois que vous aurez défini la politique, les règles métiers, les données Cette décision ne sera pas irrévocable, mais difficile à contourner.
critiques et entités, et que vous aurez fait le choix de développer en
âme et conscience votre nouvelle création en utilisant Django, vous
serez bon gré mal gré, contraint de continuer avec. Cette décision ne
sera pas irrévocable, mais difficile à contourner.
\begin{quote} \begin{quote}
At some point in their history most DevOps organizations were hobbled by At some point in their history most DevOps organizations were hobbled by
tightly-coupled, monolithic architectures that while extremely tightly-coupled, monolithic architectures that while extremely
successfull at helping them achieve product/market fit - put them at successfull at helping them achieve product/market fit - put them at
risk of organizational failure once they had to operate at scale (e.g. risk of organizational failure once they had to operate at scale (e.g.
eBay's monolithic C++ application in 2001, Amazon's monolithic OBIDOS eBay's monolithic C++ application in 2001, Amazon's monolithic OBIDOS
application in 2001, Twitter's monolithic Rails front-end in 2009, and application in 2001, Twitter's monolithic Rails front-end in 2009, and
LinkedIn's monolithic Leo application in 2011). In each of these cases, LinkedIn's monolithic Leo application in 2011). In each of these cases,
they were able to re-architect their systems and set the stage not only they were able to re-architect their systems and set the stage not only
to survice, but also to thrise and win in the marketplace. to survice, but also to thrise and win in the marketplace.
\cite[182]{devops_handbook} \cite[182]{devops_handbook}
\end{quote} \end{quote}
Ceci dit, Django compense ses contraintes en proposant énormément de Ceci dit, Django compense ses contraintes en proposant énormément de flexibilité et de fonctionnalités \textbf{out-of-the-box}, c'est-à-dire que vous pourrez sans doute avancer vite et bien jusqu'à un point de rupture, puis revoir la conception et réinvestir à ce moment-là, mais en toute connaissance de cause.
flexibilité et de fonctionnalités \textbf{out-of-the-box}, c'est-à-dire
que vous pourrez sans doute avancer vite et bien jusqu'à un point de
rupture, puis revoir la conception et réinvestir à ce moment-là, mais en
toute connaissance de cause.
\begin{quote} \begin{quote}
When any of the external parts of the system become obsolete, such as When any of the external parts of the system become obsolete, such as
the database, or the web framework, you can replace those obsolete the database, or the web framework, you can replace those obsolete
elements with a minimum of fuss. elements with a minimum of fuss.
--- Robert C. Martin Clean Architecture --- Robert C. Martin Clean Architecture
\end{quote} \end{quote}
Avec Django, la difficulté à se passer du framework va consister à Avec Django, la difficulté à se passer du framework va consister à basculer vers «~autre chose~» et a remplacer chacune des tentacules qui aura pousser partout dans l'application.
basculer vers «~autre chose~» et a remplacer chacune des tentacules qui
aura pousser partout dans l'application.
A noter que les services et les «~architectures orientées services~» ne A noter que les services et les «~architectures orientées services~» ne sont jamais qu'une définition d'implémentation des frontières, dans la mesure où un service n'est jamais qu'une fonction appelée au travers d'un protocole (rest, soap, \ldots\hspace{0pt}).
sont jamais qu'une définition d'implémentation des frontières, dans la Une application monolotihique sera tout aussi fonctionnelle qu'une application découpée en microservices. \cite[p. 243]{clean_architecture}
mesure où un service n'est jamais qu'une fonction appelée au travers
d'un protocole (rest, soap, \ldots\hspace{0pt}). Une application
monolotihique sera tout aussi fonctionnelle qu'une application découpée
en microservices. \cite[p. 243]{clean_architecture}
\section{Inversion de dépendances} \section{Inversion de dépendances}
Dans la partie SOLID, nous avons évoqué plusieurs principes de Dans la partie SOLID, nous avons évoqué plusieurs principes de développement.
développement. Django est un framework qui évolue, et qui a pu présenter Django est un framework qui évolue, et qui a pu présenter certains problèmes liés à l'un de ces principes.
certains problèmes liés à l'un de ces principes.
Les \href{https://docs.djangoproject.com/en/2.0/releases/2.0/}{Releases Notes} de Django 2.0 date de décembre 2017; parmi ces notes, l'une d'elles cite l'abandon du support d'\href{https://docs.djangoproject.com/en/2.0/releases/2.0/\#dropped-support-for-oracle-11-2}{Oracle 11.2}. Les \href{https://docs.djangoproject.com/en/2.0/releases/2.0/}{Releases Notes} de Django 2.0 date de décembre 2017; parmi ces notes, l'une d'elles cite l'abandon du support d'\href{https://docs.djangoproject.com/en/2.0/releases/2.0/\#dropped-support-for-oracle-11-2}{Oracle 11.2}.
En substance, cela signifie que le framework se chargeait lui-même de construire certaines parties de requêtes, qui deviennent non fonctionnelles dès lors que l'on met le framework ou le moteur de base de données à jour. En substance, cela signifie que le framework se chargeait lui-même de construire certaines parties de requêtes, qui deviennent non fonctionnelles dès lors que l'on met le framework ou le moteur de base de données à jour.
Réécrit, cela signifie que: Réécrit, cela signifie que:
\begin{enumerate} \begin{enumerate}
\item Si vos données sont stockées dans un moteur géré par Oracle 11.2, vous serez limité à une version 1.11 de Django \item
\item Tandis que si votre moteur est géré par une version ultérieure, le framework pourra être mis à jour. Si vos données sont stockées dans un moteur géré par Oracle 11.2, vous serez limité à une version 1.11 de Django
\item
Tandis que si votre moteur est géré par une version ultérieure, le framework pourra être mis à jour.
\end{enumerate} \end{enumerate}
Nous sommes dans un cas concret d'inversion de dépendances ratée: le framework (et encore moins vos politiques et règles métiers) ne devraient pas avoir connaissance du moteur de base de données. Nous sommes dans un cas concret d'inversion de dépendances ratée: le framework (et encore moins vos politiques et règles métiers) ne devraient pas avoir connaissance du moteur de base de données.
Pire, vos politiques et données métiers ne devraient pas avoir connaissance \textbf{de la version} du moteur de base de données. Pire, vos politiques et données métiers ne devraient pas avoir connaissance \textbf{de la version} du moteur de base de données.
En conclusion, le choix d'une version d'un moteur technique (\textbf{la base de données}) a une incidence directe sur les fonctionnalités mises à disposition par votre application, ce qui va à l'encontre des 12 facteurs (et des principes de développement). En conclusion, le choix d'une version d'un moteur technique (\textbf{la base de données}) a une incidence directe sur les fonctionnalités mises à disposition par votre application, ce qui va à l'encontre des 12 facteurs (et des principes de développement).
@ -887,14 +862,14 @@ La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais l
-- Antoine de Saint-Exupéry -- Antoine de Saint-Exupéry
\end{quote} \end{quote}
Il est impossible de se projeter dans le futur d'une application: il est impossible d'imaginer les modifications qui seront demandées par les utilisateurs, de se projeter dans l'évolution d'un langage, dans les nécessités d'intégration de certaines librairies ou dans le support-même de certaines fonctionnalités par les navigateurs Web. Il est impossible de se projeter dans le futur d'une application: il est impossible d'imaginer les modifications qui seront demandées par les utilisateurs, de se projeter dans l'évolution d'un langage, dans les nécessités d'intégration de certaines librairies ou dans le support-même de certaines fonctionnalités par les navigateurs Web.
Ce sont des choses qui viennent avec l'expérience (ou avec la tordure d'esprit \footnote{Si, ça existe}). Ce sont des choses qui viennent avec l'expérience (ou avec la tordure d'esprit \footnote{Si, ça existe}).
Cela rejoint le fameux "YAGNI\index{YAGNI}" dont nous parlions plus tôt: il est inutile de vouloir développer absolument toutes les fonctionnalités qui pourraient un jour pouvoir être utilisées ou souhaitées, car cela complexifiera autant le code, que les déploiement, l'utilisabilité ou la compréhension que les utilisateurs pourront avoir de votre application. \cite{rework} Cela rejoint le fameux "YAGNI\index{YAGNI}" dont nous parlions plus tôt: il est inutile de vouloir développer absolument toutes les fonctionnalités qui pourraient un jour pouvoir être utilisées ou souhaitées, car cela complexifiera autant le code, que les déploiement, l'utilisabilité ou la compréhension que les utilisateurs pourront avoir de votre application. \cite{rework}
Il est impossible d'imaginer ou de se projeter dans tous les éléments qui pourraient devoir être modifiés après que votre développement ait été livrée. Il est impossible d'imaginer ou de se projeter dans tous les éléments qui pourraient devoir être modifiés après que votre développement ait été livrée.
En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite, lidée est de pousser le développement jusquau point où une décision pourrait devoir être faite. En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite, lidée est de pousser le développement jusquau point où une décision pourrait devoir être faite.
A ce stade, larchitecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe. A ce stade, larchitecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe.
Nous nallons donc pas jusquau point où le service doit être créé (même sil peut ne pas être nécessaire), ni à lextrême au fait dignorer quun service pourrait être nécessaire, mais nous aboutissons à une forme de compromis. Nous nallons donc pas jusquau point où le service doit être créé (même sil peut ne pas être nécessaire), ni à lextrême au fait dignorer quun service pourrait être nécessaire, mais nous aboutissons à une forme de compromis.
Une forme d'application de la philosophie de René Descartes, où le fait de seulement envisager une possibilité ouvre un maximum de portes. Une forme d'application de la philosophie de René Descartes, où le fait de seulement envisager une possibilité ouvre un maximum de portes.
Avec cette approche, les composants seront déjà découplés au mieux. Avec cette approche, les composants seront déjà découplés au mieux.
@ -902,76 +877,76 @@ Avec cette approche, les composants seront déjà découplés au mieux.
Les composants peuvent être découpés au niveau: Les composants peuvent être découpés au niveau:
\begin{itemize} \begin{itemize}
\item \textbf{Du code source}, via des modules, paquets, dépendances, ... \item
\item \textbf{Du déploiement ou de l'exécution}, au travers de dll, jar, linked libraries, ..., voire au travers de threads ou de processus locaux. \textbf{Du code source}, via des modules, paquets, dépendances, \ldots
\item \textbf{Via la mise à disposition de nouveaux services}, lorsqu'il est plus intuitif de contacter un nouveau point de terminaison que d'intégrer de force de nouveaux concepts dans une base de code existante. \item
\textbf{Du déploiement ou de l'exécution}, au travers de dll, jar, linked libraries, \ldots, voire au travers de threads ou de processus locaux.
\item
\textbf{Via la mise à disposition de nouveaux services}, lorsqu'il est plus intuitif de contacter un nouveau point de terminaison que d'intégrer de force de nouveaux concepts dans une base de code existante.
\end{itemize} \end{itemize}
Cette section se base sur deux ressources principales \cite{maintainable_software} \cite{clean_code}, qui répartissent un ensemble de conseils parmi quatre niveaux de composants: Cette section se base sur deux ressources principales \cite{maintainable_software} \cite{clean_code}, qui répartissent un ensemble de conseils parmi quatre niveaux de composants:
\begin{itemize} \begin{itemize}
\item Les méthodes et fonctions \item Les méthodes et fonctions
\item Les classes \item Les classes
\item Les composants \item Les composants
\item Et des conseils plus généraux. \item Et des conseils plus généraux.
\end{itemize} \end{itemize}
\subsection{Au niveau des méthodes et fonctions} \subsection{Au niveau des méthodes et fonctions}
\begin{itemize} \begin{itemize}
\item \item
\textbf{Gardez vos méthodes/fonctions courtes}. Pas plus de 15 lignes, en comptant les commentaires. \textbf{Gardez vos méthodes/fonctions courtes}.
Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9\% de plus de 60 lignes; pas plus de 22.3\% de plus de 30 lignes, au plus 43.7\% de plus de 15 lignes et au moins 56.3\% en dessous de 15 lignes). Pas plus de 15 lignes, en comptant les commentaires.
Oui, c'est dur à tenir, mais faisable. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9\% de plus de 60 lignes; pas plus de 22.3\% de plus de 30 lignes, au plus 43.7\% de plus de 15 lignes et au moins 56.3\% en dessous de 15 lignes).
\item Oui, c'est dur à tenir, mais faisable.
\textbf{Conserver une complexité de McCabe en dessous de 5}, c'est-à-dire avec quatre branches au maximum. \item
A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions ayant chacune une complexité de 5 conservera la complexité globale à 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable. \textbf{Conserver une complexité de McCabe en dessous de 5}, c'est-à-dire avec quatre branches au maximum.
\item A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions ayant chacune une complexité de 5 conservera la complexité globale à 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
\textbf{N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc.}: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées. \item
\item \textbf{N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc.}: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées.
\textbf{Conservez de petites interfaces et signatures de fonctions/méthodes}. Quatre paramètres, pas plus. \item
Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester. \textbf{Conservez de petites interfaces et signatures de fonctions/méthodes}.
Quatre paramètres, pas plus.
Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester.
\end{itemize} \end{itemize}
\subsection{Au niveau des classes} \subsection{Au niveau des classes}
\begin{itemize} \begin{itemize}
\item \item
\textbf{Privilégiez un couplage faible entre vos classes}. \textbf{Privilégiez un couplage faible entre vos classes}.
Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences respectif. Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences respectif.
L'implémentation du service \texttt{UserNotificationsService} ne doit pas forcément se trouver embarqué dans une classe \texttt{UserService}. L'implémentation du service \texttt{UserNotificationsService} ne doit pas forcément se trouver embarqué dans une classe \texttt{UserService}.
De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction. De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction.
La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée. La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée.
\end{itemize} \end{itemize}
Dans la même veine, faites en sorte que les dépendances aillent toutes "dans le même sens", ce qui limitera l'effet spaghetti associé au code, tout en améliorant sa lisibilité et l'intuitivité de sa compréhension. Dans la même veine, faites en sorte que les dépendances aillent toutes "dans le même sens", ce qui limitera l'effet spaghetti associé au code, tout en améliorant sa lisibilité et l'intuitivité de sa compréhension.
\subsection{Au niveau des composants} \subsection{Au niveau des composants}
\begin{itemize} \begin{itemize}
\item \item
\textbf{Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants} également. \textbf{Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants} également.
Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'il ne soit possible de contacter trop facilement des couches séparées de l'architecture. Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'il ne soit possible de contacter trop facilement des couches séparées de l'architecture.
Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "\emph{juste parce qu'elle en a la possibilité}". Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "\emph{juste parce qu'elle en a la possibilité}".
Vous pourriez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que \texttt{IFileTransfer} avec ses méthodes \texttt{put} et \texttt{get}, et non pas les détailsd'implémentation complet d'une classe \texttt{FtpFileTransfer} ou \texttt{SshFileTransfer}). Vous pourriez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que \texttt{IFileTransfer} avec ses méthodes \texttt{put} et \texttt{get}, et non pas les détailsd'implémentation complet d'une classe \texttt{FtpFileTransfer} ou \texttt{SshFileTransfer}).
\item \item
\textbf{Conserver un bon balancement au niveau des composants}: évitez qu'un composant \textbf{A} ne soit un énorme mastodonte, alors que le \textbf{Conserver un bon balancement au niveau des composants}: évitez qu'un composant \textbf{A} ne soit un énorme mastodonte, alors que le composant juste à côté ne soit capable que d'une action.
composant juste à côté ne soit capable que d'une action. De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités seront plus faciles à gérer.
De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités seront plus faciles à Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que chacun de ces composants soit approximativement de même taille.
gérer.
Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que chacun de ces composants soit approximativement de même taille.
\end{itemize} \end{itemize}
\subsection{De manière générale} \subsection{De manière générale}
\begin{itemize} \begin{itemize}
\item \item
\textbf{Conserver une densité de code faible}: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en \textbf{Conserver une densité de code faible} : il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois.
moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisantdu refactoring (intensif), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux.
Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisantdu refactoring (intensif), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux. Mais surtout, en évitant de copier/coller bêtement du code existant.
Mais surtout, en évitant de copier/coller bêtement du code existant. \item
\item \textbf{Automatiser les tests}, en ajoutant un environnement d'intégration continue dès le début du projet et en faisant vérifier par des outils automatiques tous les points ci-dessus.
\textbf{Automatiser les tests}, en ajoutant un environnement d'intégration continue dès le début du projet et en faisant vérifier par des outils automatiques tous les points ci-dessus.
\end{itemize} \end{itemize}

View File

@ -1,54 +1,50 @@
\chapter{Authentification} \chapter{Authentification}
Comme on l'a vu dans la partie sur le modèle, nous souhaitons que le créateur d'une liste puisse retrouver facilement les éléments qu'il aura créée. Comme on l'a vu dans la partie sur le modèle, nous souhaitons que le créateur d'une liste puisse retrouver facilement les éléments qu'il aura créée.
Ce dont nous n'avons pas parlé cependant, c'est la manière dont l'utilisateur va pouvoir créer son compte et s'authentifier. Ce dont nous n'avons pas parlé cependant, c'est la manière dont l'utilisateur va pouvoir créer son compte et s'authentifier.
La \href{https://docs.djangoproject.com/en/stable/topics/auth/}{documentation} est très complète (et un peu complexe), nous allons essayer de la simplifier au maximum. La \href{https://docs.djangoproject.com/en/stable/topics/auth/}{documentation} est très complète (et un peu complexe), nous allons essayer de la simplifier au maximum.
Accrochez-vous, le sujet peut être tendu lors d'une première exploration. Accrochez-vous, le sujet peut être tendu lors d'une première exploration.
\section{Utilisateurs} \section{Utilisateurs}
Dans les spécifications, nous souhaitions pouvoir associer un utilisateur à une liste (\textbf{le propriétaire}) et un utilisateur à une part (\textbf{le donateur}). Dans les spécifications, nous souhaitions pouvoir associer un utilisateur à une liste (\textbf{le propriétaire}) et un utilisateur à une part (\textbf{le donateur}).
Par défaut, Django offre une gestion simplifiée des utilisateurs (pas de connexion LDAP, pas de double Par défaut, Django offre une gestion simplifiée des utilisateurs (pas de connexion LDAP, pas de double authentification, \ldots~: juste un utilisateur et un mot de passe.
authentification, ...: juste un utilisateur et un mot de passe. Pour y accéder, un paramètre par défaut est défini dans votre fichier de settings : \texttt{AUTH\_USER\_MODEL}.
Pour y accéder, un paramètre par défaut est défini dans votre fichier de settings: \texttt{AUTH\_USER\_MODEL}.
Toute modification de la modélisation des utilisateurs exige qu'un modèle personnalisé existe. Toute modification de la modélisation des utilisateurs exige qu'un modèle personnalisé existe.
La première difficulté est que toute modification en cours de route de la modélisation des utilisateurs La première difficulté est que toute modification en cours de route de la modélisation des utilisateurs
\section{Mécanisme d'authentification} \section{Mécanisme d'authentification}
On peut schématiser le flux d'authentification de la manière suivante : On peut schématiser le flux d'authentification de la manière suivante :
En gros: En gros:
\begin{enumerate} \begin{enumerate}
\item \item
La personne accède à une URL qui est protégée (voir les décorateurs @login\_required et le mixin LoginRequiredMixin) La personne accède à une URL qui est protégée (voir les décorateurs @login\_required et le mixin LoginRequiredMixin)
\item \item
Le framework détecte qu'il est nécessaire pour la personne de se connecter (grâce à un paramètre type LOGIN\_URL) Le framework détecte qu'il est nécessaire pour la personne de se connecter (grâce à un paramètre type LOGIN\_URL)
\item \item
Le framework présente une page de connexion ou un mécanisme d'accès pour la personne (template à définir) Le framework présente une page de connexion ou un mécanisme d'accès pour la personne (template à définir)
\item \item
Le framework récupère les informations du formulaire, et les transmets aux différents backends d'authentification, dans l'ordre Le framework récupère les informations du formulaire, et les transmets aux différents backends d'authentification, dans l'ordre
\item \item
Chaque backend va appliquer la méthode \texttt{authenticate} en cascade, jusqu'à ce qu'un backend réponde True ou qu'aucun ne réponde Chaque backend va appliquer la méthode \texttt{authenticate} en cascade, jusqu'à ce qu'un backend réponde True ou qu'aucun ne réponde
\item \item
La réponse de la méthode authenticate doit être une instance d'un utilisateur, tel que définit parmi les paramètres généraux de l'application. La réponse de la méthode authenticate doit être une instance d'un utilisateur, tel que définit parmi les paramètres généraux de l'application.
\end{enumerate} \end{enumerate}
En résumé (bis): En résumé (bis):
\begin{enumerate} \begin{enumerate}
\item \item
Une personne souhaite se connecter; Une personne souhaite se connecter;
\item \item
Les backends d'authentification s'enchaîne jusqu'à trouver une bonne Les backends d'authentification s'enchaîne jusqu'à trouver une bonne correspondance. Si aucune correspondance n'est trouvée, on envoie la personne sur les roses.
correspondance. Si aucune correspondance n'est trouvée, on envoie la \item
personne sur les roses. Si OK, on retourne une instance de type current\_user, qui pourra être utilisée de manière uniforme dans l'application.
\item
Si OK, on retourne une instance de type current\_user, qui pourra être
utilisée de manière uniforme dans l'application.
\end{enumerate} \end{enumerate}
Ci-dessous, on définit deux backends différents pour mieux comprendre les différentes possibilités: Ci-dessous, on définit deux backends différents pour mieux comprendre les différentes possibilités:
@ -81,14 +77,14 @@ class TokenBackend(backends.ModelBackend):
current_token = Token.objects.filter( current_token = Token.objects.filter(
token=token, validity_date__gte=datetime.now() token=token, validity_date__gte=datetime.now()
).first() ).first()
if current_token: if current_token:
user = current_token.user user = current_token.user
current_token.last_used_date = datetime.now() current_token.last_used_date = datetime.now()
current_token.save() current_token.save()
return user return user
return None return None
\end{minted} \end{minted}
@ -115,17 +111,17 @@ class LdapBackend(backends.ModelBackend):
""" """
ldap_server = Server(settings.LDAP_SERVER, get_info=ALL) ldap_server = Server(settings.LDAP_SERVER, get_info=ALL)
ldap_connection = Connection( ldap_connection = Connection(
ldap_server, ldap_server,
user=username, user=username,
password=password password=password
) )
try: try:
if not ldap_connection.bind(): if not ldap_connection.bind():
raise ValueError("Login ou mot de passe incorrect") raise ValueError("Login ou mot de passe incorrect")
except (LDAPPasswordIsMandatoryError, ValueError) as ldap_exception: except (LDAPPasswordIsMandatoryError, ValueError) as ldap_exception:
raise ldap_exception raise ldap_exception
user, _ = UserModel.objects.get_or_create(username=username) user, _ = UserModel.objects.get_or_create(username=username)
return user return user
@ -140,43 +136,50 @@ class LdapBackend(backends.ModelBackend):
On peut résumer le mécanisme d'authentification de la manière suivante: On peut résumer le mécanisme d'authentification de la manière suivante:
\begin{itemize} \begin{itemize}
\item \item
Si vous voulez modifier les informations liées à un utilisateur, orientez-vous vers la modification du modèle. Comme nous le verrons ci-dessous, il existe trois manières de prendre ces modifications en Si vous voulez modifier les informations liées à un utilisateur, orientez-vous vers la modification du modèle. Comme nous le verrons ci-dessous, il existe trois manières de prendre ces modifications en compte. Voir également \href{https://docs.djangoproject.com/en/stable/topics/auth/customizing/}{ici}.
compte. Voir également \href{https://docs.djangoproject.com/en/stable/topics/auth/customizing/}{ici}. \item
\item Si vous souhaitez modifier la manière dont l'utilisateur se connecte, alors vous devrez modifier le \textbf{backend}.
Si vous souhaitez modifier la manière dont l'utilisateur se connecte, alors vous devrez modifier le \textbf{backend}.
\end{itemize} \end{itemize}
\section{Modélisation} \section{Modélisation}
Dans un premier temps, Django a besoin de manipuler \href{https://docs.djangoproject.com/en/1.9/ref/contrib/auth/\#user-model}{des instances de type \texttt{django.contrib.auth.User}}. Cette classe implémente les champs suivants: Dans un premier temps, Django a besoin de manipuler \href{https://docs.djangoproject.com/en/1.9/ref/contrib/auth/\#user-model}{des instances de type \texttt{django.contrib.auth.User}}.
Cette classe implémente les champs suivants:
\begin{itemize} \begin{itemize}
\item \item
\texttt{username} \texttt{username}
\item \item
\texttt{first\_name} \texttt{first\_name}
\item \item
\texttt{last\_name} \texttt{last\_name}
\item \item
\texttt{email} \texttt{email}
\item \item
\texttt{password} \texttt{password}
\item \item
\texttt{date\_joined}. \texttt{date\_joined}.
\end{itemize} \end{itemize}
D'autres champs, comme les groupes auxquels l'utilisateur est associé, ses permissions, savoir s'il est un super-utilisateur, \ldots\hspace{0pt} sont moins pertinents pour le moment. Avec les quelques champs déjà définis ci-dessus, nous avons de quoi identifier correctement nos utilisateurs. Inutile d'implémenter nos propres classes, puisqu'elles existent déjà. D'autres champs, comme les groupes auxquels l'utilisateur est associé, ses permissions, savoir s'il est un super-utilisateur, \ldots\hspace{0pt} sont moins pertinents pour le moment.
Avec les quelques champs déjà définis ci-dessus, nous avons de quoi identifier correctement nos utilisateurs.
Inutile d'implémenter nos propres classes, puisqu'elles existent déjà.
Si vous souhaitez ajouter un champ, il existe trois manières de faire. Si vous souhaitez ajouter un champ, il existe trois manières de faire.
\subsection{Extension du modèle existant} \subsection{Extension du modèle existant}
Le plus simple consiste à créer une nouvelle classe, et à faire un lien de type \texttt{OneToOne} vers la classe \texttt{django.contrib.auth.User}. De cette manière, on ne modifie rien à la manière dont Django authentife ses utlisateurs: tout ce qu'on fait, c'est un lien vers une table nouvellement créée, comme on l'a déjà vu au point {[}\ldots\hspace{0pt}voir l'héritage de modèle{]}. L'avantage de cette méthode, c'est qu'elle est extrêmement flexible, et qu'on garde les mécanismes Django standard. Le désavantage, c'est que pour avoir toutes les informations de notre utilisateur, on sera obligé d'effectuer une jointure sur le base de données, ce qui pourrait avoir des conséquences sur les performances. Le plus simple consiste à créer une nouvelle classe, et à faire un lien de type \texttt{OneToOne} vers la classe \texttt{django.contrib.auth.User}.
De cette manière, on ne modifie rien à la manière dont Django authentife ses utlisateurs: tout ce qu'on fait, c'est un lien vers une table nouvellement créée, comme on l'a déjà vu au point {[}\ldots\hspace{0pt}voir l'héritage de modèle{]}.
L'avantage de cette méthode, c'est qu'elle est extrêmement flexible, et qu'on garde les mécanismes Django standard.
Le désavantage, c'est que pour avoir toutes les informations de notre utilisateur, on sera obligé d'effectuer une jointure sur le base de données, ce qui pourrait avoir des conséquences sur les performances.
\subsection{Substitution du modèle} \subsection{Substitution du modèle}
Avant de commencer, sachez que cette étape doit être effectuée \textbf{avant la première migration}. Le plus simple sera de définir une nouvelle classe héritant de \texttt{django.contrib.auth.User} et de spécifier la classe à utiliser dans votre fichier de paramètres. Si ce paramètre est modifié après que la première migration ait été effectuée, il ne sera pas pris en compte. Tenez-en compte au moment de modéliser votre application. Avant de commencer, sachez que cette étape doit être effectuée \textbf{avant la première migration}.
Le plus simple sera de définir une nouvelle classe héritant de \texttt{django.contrib.auth.User} et de spécifier la classe à utiliser dans votre fichier de paramètres.
Si ce paramètre est modifié après que la première migration ait été effectuée, il ne sera pas pris en compte.
Tenez-en compte au moment de modéliser votre application.
\begin{minted}{python} \begin{minted}{python}
AUTH_USER_MODEL = 'myapp.MyUser' AUTH_USER_MODEL = 'myapp.MyUser'
@ -186,29 +189,33 @@ Avant de commencer, sachez que cette étape doit être effectuée \textbf{avant
Notez bien qu'il ne faut pas spécifier le package \texttt{.models} dans cette injection de dépendances: le schéma à indiquer est bien \texttt{\textless{}nom\ de\ lapplication\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}. Notez bien qu'il ne faut pas spécifier le package \texttt{.models} dans cette injection de dépendances: le schéma à indiquer est bien \texttt{\textless{}nom\ de\ lapplication\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}.
\subsection{OAuth} \subsection{OAuth}
OAuth est un standard libre définissant un ensemble de méthodes à implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement se base sur un système de jetons (Tokens), attribués par le possesseur de la ressource à laquelle un utilisateur souhaite accéder. OAuth est un standard libre définissant un ensemble de méthodes à implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement se base sur un système de jetons (Tokens), attribués par le possesseur de la ressource à laquelle un utilisateur souhaite accéder.
Le client initie la connexion en demandant un jeton au serveur. Ce jeton est ensuite utilisée tout au long de la connexion, pour accéder aux différentes ressources offertes par ce serveur. `wikipedia \textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}. Le client initie la connexion en demandant un jeton au serveur.
Ce jeton est ensuite utilisée tout au long de la connexion, pour accéder aux différentes ressources offertes par ce serveur.
`wikipedia \textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}.
Une introduction à OAuth est \href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}. Elle Une introduction à OAuth est \href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}.
introduit le protocole comme étant une \texttt{valet\ key}, une clé que l'on donne à la personne qui va garer votre voiture pendant que vous profitez des mondanités. Cette clé donne un accès à votre voiture, tout Elle introduit le protocole comme étant une \texttt{valet\ key}, une clé que l'on donne à la personne qui va garer votre voiture pendant que vous profitez des mondanités.
en bloquant un ensemble de fonctionnalités. Le principe du protocole est semblable en ce sens: vous vous réservez un accès total à une API, tandis que le système de jetons permet d'identifier une personne, tout Cette clé donne un accès à votre voiture, tout en bloquant un ensemble de fonctionnalités.
en lui donnant un accès restreint à votre application. Le principe du protocole est semblable en ce sens: vous vous réservez un accès total à une API, tandis que le système de jetons permet d'identifier une personne, tout en lui donnant un accès restreint à votre application.
L'utilisation de jetons permet notamment de définir une durée d'utilisation et une portée d'utilisation. L'utilisateur d'un service A peut par exemple autoriser un service B à accéder à des ressources qu'il L'utilisation de jetons permet notamment de définir une durée d'utilisation et une portée d'utilisation.
L'utilisateur d'un service A peut par exemple autoriser un service B à accéder à des ressources qu'il
possède, sans pour autant révéler son nom d'utilisateur ou son mot de passe. possède, sans pour autant révéler son nom d'utilisateur ou son mot de passe.
L'exemple repris au niveau du \href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site faji.com (A). Elle souhaite les imprimer au travers du site beppa.com (B). Au moment de la commande, le site beppa.com envoie une demande au site faji.com pour accéder aux ressources partagées par Jane. Pour cela, une nouvelle page s'ouvre pour l'utilisateur, et lui demande d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui permet d'y accéder. L'exemple repris au niveau du \href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site faji.com (A).
Elle souhaite les imprimer au travers du site beppa.com (B).
Au moment de la commande, le site beppa.com envoie une demande au site faji.com pour accéder aux ressources partagées par Jane.
Pour cela, une nouvelle page s'ouvre pour l'utilisateur, et lui demande d'introduire sa "pièce d'identité".
Le site A, ayant reçu une demande de B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui permet d'y accéder.
\section{Templates} \section{Templates}
Ce qui n'existe pas par contre, ce sont les vues. Django propose donc Ce qui n'existe pas par contre, ce sont les vues.
tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors Django propose donc tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors administration).
administration). En premier lieu, ces paramètres sont fixés dans le En premier lieu, ces paramètres sont fixés dans le fichier `settings \textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}.
fichier `settings On y trouve par exemple les paramètres suivants :
\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}.
On y trouve par exemple les paramètres suivants:
\begin{itemize} \begin{itemize}
\item \item

View File

@ -1,4 +1,6 @@
\chapter{Context Processors} \chapter{Context Processors}
Mise en pratique: un \emph{context processor} sert \emph{grosso-modo} à peupler l'ensemble des données transmises des vues aux templates avec des données communes.
Un context processor est un peu l'équivalent d'un middleware, mais entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
Un \emph{context processor} sert \emph{grosso-modo} à peupler l'ensemble des données transmises des vues aux templates avec des données communes. Un context processor est un peu l'équivalent d'un middleware, mais est situé entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes. Un \emph{context processor} sert \emph{grosso-modo} à peupler l'ensemble des données transmises des vues aux templates avec des données communes. Un context processor est un peu l'équivalent d'un middleware, mais est situé entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
@ -41,5 +43,5 @@ Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe} et \tex
] ]
\end{minted} \end{minted}
Les context processors sont extrêmement utiles pour injecter des données dans chacune des vues. Les context processors sont extrêmement utiles pour injecter des données dans chacune des vues.
Il peut cependant être utile d'utiliser un mécanisme de cache pour gagner du temps de traitement, surtout lorsque des appels en base de données doivent être réalisés: ceci évite que ces appels ne soient réalisés trop souvent, alors qu'ils n'évoluent peut-être pas aussi vite. Il peut cependant être utile d'utiliser un mécanisme de cache pour gagner du temps de traitement, surtout lorsque des appels en base de données doivent être réalisés: ceci évite que ces appels ne soient réalisés trop souvent, alors qu'ils n'évoluent peut-être pas aussi vite.

View File

@ -1,6 +1,7 @@
\chapter{Debian} \chapter{Debian}
La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits. Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur \textbf{root}, car la moindre faille pourrait avoir des conséquences catastrophiques. La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits.
Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur \textbf{root}, car la moindre faille pourrait avoir des conséquences catastrophiques.
Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'étape de configuration, qui consistera à: Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'étape de configuration, qui consistera à:
@ -15,99 +16,90 @@ Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'éta
Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes d'un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l'un des travailleurs. Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes d'un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l'un des travailleurs.
\end{enumerate} \end{enumerate}
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, ... Il existe des dizaines d'hébergements typés VPS (\textbf{Virtual Private Server}). A vous de choisir celui qui vous convient \footnote{Personnellement, j'ai un petit faible pour Hetzner Cloud}. La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, \ldots~
Il existe des dizaines d'hébergements typés VPS (\textbf{Virtual Private Server}).
A vous de choisir celui qui vous convient \footnote{Personnellement, j'ai un petit faible pour Hetzner Cloud}.
\begin{verbatim} \begin{verbatim}
apt update apt update
groupadd --system webapps groupadd --system webapps
groupadd --system gunicorn_sockets groupadd --system gunicorn_sockets
useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift
mkdir -p /home/gwift mkdir -p /home/gwift
chown gwift:webapps /home/gwift chown gwift:webapps /home/gwift
\end{verbatim} \end{verbatim}
\begin{itemize} \begin{itemize}
\item \item
On ajoute un groupe intitulé \texttt{webapps} On ajoute un groupe intitulé \texttt{webapps}
\item \item
On crée un groupe pour les communications via sockets On crée un groupe pour les communications via sockets
\item \item
On crée notre utilisateur applicatif; ses applications seront placées On crée notre utilisateur applicatif; ses applications seront placées dans le répertoire \texttt{/home/gwift}
dans le répertoire \texttt{/home/gwift} \item
\item On crée le répertoire home/gwift
On crée le répertoire home/gwift \item
\item On donne les droits sur le répertoire /home/gwift
On donne les droits sur le répertoire /home/gwift
\end{itemize} \end{itemize}
\section{Dépendances systèmes} \section{Dépendances systèmes}
La version 3.6 de Python se trouve dans les dépôts officiels de CentOS. La version 3.6 de Python se trouve dans les dépôts officiels de CentOS. Si vous souhaitez utiliser une version ultérieure, il suffit de l'installer en parallèle de la version officiellement supportée par votre distribution.
Si vous souhaitez utiliser une version ultérieure, il suffit de
l'installer en parallèle de la version officiellement supportée par
votre distribution.
Pour CentOS, vous avez donc deux possibilités : Pour CentOS, vous avez donc deux possibilités :
\begin{verbatim} \begin{verbatim}
yum install python36 -y yum install python36 -y
\end{verbatim} \end{verbatim}
Ou passer par une installation alternative: Ou passer par une installation alternative:
\begin{verbatim} \begin{verbatim}
sudo yum -y groupinstall "Development Tools" sudo yum -y groupinstall "Development Tools"
sudo yum -y install openssl-devel bzip2-devel libffi-devel sudo yum -y install openssl-devel bzip2-devel libffi-devel
wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz
cd Python-3.8*/ cd Python-3.8*/
./configure --enable-optimizations ./configure --enable-optimizations
sudo make altinstall sudo make altinstall
\end{verbatim} \end{verbatim}
\begin{itemize} \begin{itemize}
\item \item
\textbf{Attention !} Le paramètre \texttt{altinstall} est primordial. \textbf{Attention !} Le paramètre \texttt{altinstall} est primordial.
Sans lui, vous écraserez l'interpréteur initialement supporté par la Sans lui, vous écraserez l'interpréteur initialement supporté par la distribution, et cela pourrait avoir des effets de bord non souhaités.
distribution, et cela pourrait avoir des effets de bord non souhaités.
\end{itemize} \end{itemize}
\section{Base de données} \section{Base de données}
On l'a déjà vu, Django se base sur un pattern type \href{https://www.martinfowler.com/eaaCatalog/activeRecord.html}{ActiveRecords} pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données connus :
On l'a déjà vu, Django se base sur un pattern type
\href{https://www.martinfowler.com/eaaCatalog/activeRecord.html}{ActiveRecords}
pour la gestion de la persistance des données et supporte les principaux
moteurs de bases de données connus:
\begin{itemize} \begin{itemize}
\item \item
SQLite (en natif, mais Django 3.0 exige une version du moteur SQLite (en natif, mais Django 3.0 exige une version du moteur supérieure ou égale à la 3.8)
supérieure ou égale à la 3.8) \item
\item MariaDB (en natif depuis Django 3.0),
MariaDB (en natif depuis Django 3.0), \item
\item PostgreSQL au travers de psycopg2 (en natif aussi),
PostgreSQL au travers de psycopg2 (en natif aussi), \item
\item Microsoft SQLServer grâce aux drivers {[}\ldots\hspace{0pt}à compléter{]}
Microsoft SQLServer grâce aux drivers {[}\ldots\hspace{0pt}à \item
compléter{]} Oracle via \href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}.
\item
Oracle via
\href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}.
\end{itemize} \end{itemize}
Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n'est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n'est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une
version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause version spécifique du pilote.
d'un Oracle 12.1). Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause d'un Oracle 12.1).
Ci-dessous, quelques procédures d'installation pour mettre un serveur à disposition.
Les deux plus simples seront MariaDB et PostgreSQL, qu'on couvrira ci-dessous.
Oracle et Microsoft SQLServer se trouveront en annexes.
Ci-dessous, quelques procédures d'installation pour mettre un serveur à disposition. Les deux plus simples seront MariaDB et PostgreSQL, qu'on couvrira ci-dessous. Oracle et Microsoft SQLServer se trouveront en
annexes.
\subsection{PostgreSQL} \subsection{PostgreSQL}
On commence par installer PostgreSQL. On commence par installer PostgreSQL.
Dans le cas de debian, on exécute la commande suivante: Dans le cas de debian, on exécute la commande suivante:
\begin{verbatim} \begin{verbatim}
@ -138,8 +130,8 @@ Finalement, on peut créer la DB:
\subsection{MariaDB} \subsection{MariaDB}
Idem, installation, configuration, backup, tout ça. A copier de grimboite, je suis sûr davoir Idem, installation, configuration, backup, tout ça.
des notes là-dessus. A copier de grimboite, je suis sûr davoir des notes là-dessus.
\section{Préparation de l'environment utilisateur} \section{Préparation de l'environment utilisateur}
@ -160,7 +152,7 @@ des notes là-dessus.
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder. La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes
suivantes: suivantes :
\begin{verbatim} \begin{verbatim}
# en tant qu'utilisateur 'gwift' # en tant qu'utilisateur 'gwift'
@ -173,37 +165,39 @@ suivantes:
=config.settings_production =config.settings_production
\end{verbatim} \end{verbatim}
\section{Configuration de l'application} \section{Configuration de l'application}
\begin{verbatim} \begin{verbatim}
SECRET_KEY=<set your secret key here> SECRET_KEY=<set your secret key here>
ALLOWED_HOSTS=* ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/gwift/static STATIC_ROOT=/var/www/gwift/static
DATABASE= DATABASE=
\end{verbatim} \end{verbatim}
\begin{itemize} \begin{itemize}
\item \item
La variable \texttt{SECRET\_KEY} est notamment utilisée pour le La variable \texttt{SECRET\_KEY} est notamment utilisée pour le
chiffrement des sessions. chiffrement des sessions.
\item \item
On fait confiance à django\_environ pour traduire la chaîne de On fait confiance à django\_environ pour traduire la chaîne de
connexion à la base de données. connexion à la base de données.
\end{itemize} \end{itemize}
\section{Création des répertoires de logs} \section{Création des répertoires de logs}
\begin{verbatim} \begin{verbatim}
mkdir -p /var/www/gwift/static mkdir -p /var/www/gwift/static
\end{verbatim} \end{verbatim}
\section{Socket} \section{Socket}
\section{Socket}
Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}: Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}:
\begin{verbatim} \begin{verbatim}
D /var/run/webapps 0775 gwift gunicorn_sockets - D /var/run/webapps 0775 gwift gunicorn_sockets -
\end{verbatim} \end{verbatim}
Suivi de la création par systemd : Suivi de la création par systemd :
@ -212,109 +206,111 @@ Suivi de la création par systemd :
systemd-tmpfiles --create systemd-tmpfiles --create
\end{verbatim} \end{verbatim}
\section{Gunicorn} \section{Gunicorn}
\begin{verbatim} \begin{verbatim}
#!/bin/bash #!/bin/bash
# defines settings for gunicorn # defines settings for gunicorn
NAME="gwift" NAME="gwift"
DJANGODIR=/home/gwift/webapps/gwift DJANGODIR=/home/gwift/webapps/gwift
SOCKFILE=/var/run/webapps/gunicorn_gwift.sock SOCKFILE=/var/run/webapps/gunicorn_gwift.sock
USER=gwift USER=gwift
GROUP=gunicorn_sockets GROUP=gunicorn_sockets
NUM_WORKERS=5 NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production DJANGO_SETTINGS_MODULE=config.settings_production
DJANGO_WSGI_MODULE=config.wsgi DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`" echo "Starting $NAME as `whoami`"
source /home/gwift/.venvs/gwift/bin/activate source /home/gwift/.venvs/gwift/bin/activate
cd $DJANGODIR cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH export PYTHONPATH=$DJANGODIR:$PYTHONPATH
exec gunicorn ${DJANGO_WSGI_MODULE}:application \ exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \ --name $NAME \
--workers $NUM_WORKERS \ --workers $NUM_WORKERS \
--user $USER \ --user $USER \
--bind=unix:$SOCKFILE \ --bind=unix:$SOCKFILE \
--log-level=debug \ --log-level=debug \
--log-file=- --log-file=-
\end{verbatim} \end{verbatim}
\section{Supervsion, keepalive et autoreload} \section{Supervsion, keepalive et autoreload}
Pour la supervision, on passe par Supervisor. Il existe d'autres Pour la supervision, on passe par Supervisor.
superviseurs, Il existe d'autres superviseurs,
\begin{verbatim} \begin{verbatim}
yum install supervisor -y yum install supervisor -y
\end{verbatim} \end{verbatim}
On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}: On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}:
\begin{verbatim} \begin{verbatim}
[program:gwift] [program:gwift]
command=/home/gwift/bin/start_gunicorn.sh command=/home/gwift/bin/start_gunicorn.sh
user=gwift user=gwift
stdout_logfile=/var/log/gwift/gwift.log stdout_logfile=/var/log/gwift/gwift.log
autostart=true autostart=true
autorestart=unexpected autorestart=unexpected
redirect_stdout=true redirect_stdout=true
redirect_stderr=true redirect_stderr=true
\end{verbatim} \end{verbatim}
Et on crée les répertoires de logs, on démarre supervisord et on vérifie Et on crée les répertoires de logs, on démarre supervisord et on vérifie
qu'il tourne correctement: qu'il tourne correctement :
\begin{verbatim} \begin{verbatim}
$ mkdir /var/log/gwift $ mkdir /var/log/gwift
$ chown gwift:nagios /var/log/gwift $ chown gwift:nagios /var/log/gwift
$ systemctl enable supervisord $ systemctl enable supervisord
$ systemctl start supervisord.service $ systemctl start supervisord.service
$ systemctl status supervisord.service $ systemctl status supervisord.service
supervisord.service - Process Monitoring and Control Daemon supervisord.service - Process Monitoring and Control Daemon
Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled; Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled;
vendor preset: disabled) vendor preset: disabled)
Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago
Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code
=exited, status=0/SUCCESS) =exited, status=0/SUCCESS)
Main PID: 2310 (supervisord) Main PID: 2310 (supervisord)
CGroup: /system.slice/supervisord.service CGroup: /system.slice/supervisord.service
- 2310 /usr/bin/python /usr/bin/supervisord -c - 2310 /usr/bin/python /usr/bin/supervisord -c
/etc/supervisord.conf /etc/supervisord.conf
- 2313 /home/gwift/.venvs/gwift/bin/python3 - 2313 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2317 /home/gwift/.venvs/gwift/bin/python3 - 2317 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2318 /home/gwift/.venvs/gwift/bin/python3 - 2318 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2321 /home/gwift/.venvs/gwift/bin/python3 - 2321 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2322 /home/gwift/.venvs/gwift/bin/python3 - 2322 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2323 /home/gwift/.venvs/gwift/bin/python3 - 2323 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
ls /var/run/webapps ls /var/run/webapps
\end{verbatim} \end{verbatim}
On peut aussi vérifier que l'application est en train de tourner, à On peut aussi vérifier que l'application est en train de tourner, à
l'aide de la commande \texttt{supervisorctl}: l'aide de la commande \texttt{supervisorctl} :
\begin{verbatim} \begin{verbatim}
supervisorctl status gwift supervisorctl status gwift
gwift RUNNING pid 31983, uptime 0:01:00 gwift RUNNING pid 31983, uptime 0:01:00
supervisorctl stop gwift supervisorctl stop gwift
gwift: stopped gwift: stopped
root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift
gwift: started gwift: started
root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift
gwift: stopped gwift: stopped
gwift: started gwift: started
\end{verbatim} \end{verbatim}
\section{Firewall}
\section{Firewall}
\begin{verbatim} \begin{verbatim}
et 443 (HTTPS). et 443 (HTTPS).
\end{verbatim} \end{verbatim}
\begin{verbatim} \begin{verbatim}
@ -324,15 +320,21 @@ l'aide de la commande \texttt{supervisorctl}:
\end{verbatim} \end{verbatim}
\begin{itemize} \begin{itemize}
\item \item
On ouvre le port 80, uniquement pour autoriser une connexion HTTP, On ouvre le port 80, uniquement pour autoriser une connexion HTTP,
mais qui sera immédiatement redirigée vers HTTPS mais qui sera immédiatement redirigée vers HTTPS
\item \item
Et le port 443 (forcément). Et le port 443 (forcément).
\end{itemize} \end{itemize}
\section{Reverse proxy}
\section{Reverse proxy}
\begin{verbatim}
yum install nginx -y
usermod -a -G gunicorn_sockets nginx
\end{verbatim}
On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}:
\begin{verbatim} \begin{verbatim}
yum install nginx -y yum install nginx -y
@ -376,7 +378,31 @@ l'aide de la commande \texttt{supervisorctl}:
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_redirect off; proxy_redirect off;
proxy_pass http://gwift_app; client_max_body_size 4G;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /static/ {
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_app;
}
} }
} }
\end{verbatim} \end{verbatim}
@ -394,58 +420,54 @@ l'aide de la commande \texttt{supervisorctl}:
\end{itemize} \end{itemize}
\section{Mise à jour} \section{Mise à jour}
\begin{verbatim} \begin{verbatim}
u - <user> u - <user>
source ~/.venvs/<app>/bin/activate source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app> cd ~/webapps/<app>
git fetch git fetch
git checkout vX.Y.Z git checkout vX.Y.Z
pip install -U requirements/prod.txt pip install -U requirements/prod.txt
python manage.py migrate python manage.py migrate
python manage.py collectstatic python manage.py collectstatic
kill -HUP `ps -C gunicorn fch -o pid | head -n 1` kill -HUP `ps -C gunicorn fch -o pid | head -n 1`
\end{verbatim} \end{verbatim}
\begin{itemize} \begin{itemize}
\item \item
\url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file} \url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file}
\end{itemize} \end{itemize}
\section{Logrotate} \section{Logrotate}
\begin{verbatim} \begin{verbatim}
/var/log/gwift/* { /var/log/gwift/* {
weekly weekly
rotate 3 rotate 3
size 10M size 10M
compress compress
delaycompress delaycompress
} }
\end{verbatim} \end{verbatim}
Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift pour vérifier que cela fonctionne correctement.
pour vérifier que cela fonctionne correctement.
\section{Sauvegardes} \section{Sauvegardes}
Les sauvegardes ont été configurées avec borg : \texttt{yum\ install\ borgbackup}.
Les sauvegardes ont été configurées avec borg:
\texttt{yum\ install\ borgbackup}.
C'est l'utilisateur gwift qui s'en occupe. C'est l'utilisateur gwift qui s'en occupe.
\begin{verbatim} \begin{verbatim}
mkdir -p /home/gwift/borg-backups/ mkdir -p /home/gwift/borg-backups/
cd /home/gwift/borg-backups/ cd /home/gwift/borg-backups/
borg init gwift.borg -e=none borg init gwift.borg -e=none
borg create gwift.borg::{now} ~/bin ~/webapps borg create gwift.borg::{now} ~/bin ~/webapps
\end{verbatim} \end{verbatim}
Et dans le fichier crontab : Et dans le fichier crontab :
\begin{verbatim} \begin{verbatim}
0 23 * * * /home/gwift/bin/backup.sh 0 23 * * * /home/gwift/bin/backup.sh
\end{verbatim} \end{verbatim}
\section{Ansible} \section{Ansible}
@ -486,4 +508,4 @@ documentation de cookie-cutter-django}.
le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt
source. source.
On peut aussi passer par fabric, ansible, chef ou puppet. On peut aussi passer par fabric, ansible, chef ou puppet.

View File

@ -5,7 +5,7 @@ Django fonctionne sur un
\section{Gestion des dépendances} \section{Gestion des dépendances}
Comme nous en avons déjà discuté, Poetry est la solution que nous avons choisie pour la gestion de nos dépendances. Comme nous en avons déjà discuté, Poetry 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{}}. 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: 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:
@ -21,21 +21,18 @@ Dans le cas de Django, et après avoir activé l'environnement, nous pouvons à
\end{verbatim} \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). 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. 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}. 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}:
Pour démarrer notre projet, nous lançons \texttt{django-admin\ startproject\ gwift}:
\begin{verbatim} \begin{verbatim}
$ django-admin startproject gwift $ django-admin startproject gwift
\end{verbatim} \end{verbatim}
Cette action a pour effet de créer un nouveau dossier \texttt{gwift}, Cette action a pour effet de créer un nouveau dossier \texttt{gwift}, dans lequel nous trouvons la structure suivante :
dans lequel nous trouvons la structure suivante:
\begin{verbatim} \begin{verbatim}
$ tree gwift $ tree gwift
gwift gwift
@ -48,36 +45,25 @@ dans lequel nous trouvons la structure suivante:
-- manage.py -- manage.py
\end{verbatim} \end{verbatim}
C'est dans ce répertoire que vont vivre tous les fichiers liés au C'est dans ce répertoire que vont vivre tous les fichiers liés au projet.
projet. Le but est de faire en sorte que toutes les opérations 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.
(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: L'utilité de ces fichiers est définie ci-dessous:
\begin{itemize} \begin{itemize}
\item \item
\texttt{settings.py} contient tous les paramètres globaux à notre \texttt{settings.py} contient tous les paramètres globaux à notre projet.
projet. \item
\item \texttt{urls.py} contient les variables de routes, les adresses utilisées et les fonctions vers lesquelles elles pointent.
\texttt{urls.py} contient les variables de routes, les adresses \item
utilisées et les fonctions vers lesquelles elles pointent. \texttt{manage.py}, pour toutes les commandes de gestion.
\item \item
\texttt{manage.py}, pour toutes les commandes de gestion. \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 \item
\texttt{asgi.py} contient la définition de l'interface \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.
\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} \end{itemize}
Indiquer qu'il est possible d'avoir plusieurs structures de dossiers et 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.
qu'il n'y a pas de "magie" derrière toutes ces commandes.
La seule condition est que les chemins référencés soient cohérents par rapport à la structure sous-jacente. La seule condition est que les chemins référencés soient cohérents par rapport à la structure sous-jacente.
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous stockerons les dépendances et un fichier README: Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous stockerons les dépendances et un fichier README:
@ -85,20 +71,20 @@ Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous sto
TODO 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. 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}. 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: 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} \begin{itemize}
\item \item
\texttt{base.txt} \texttt{base.txt}
\item \item
\texttt{dev.txt} \texttt{dev.txt}
\item \item
\texttt{production.txt} \texttt{production.txt}
\end{itemize} \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{}}. 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. 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}. 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} \begin{verbatim}
@ -109,12 +95,12 @@ Dans l'immédiat, nous allons ajouter \texttt{django} dans une version égale à
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). 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. 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. 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. 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} 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 \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é. 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. Avec les mécanismes d'intégration continue et de tests unitaires, nous verrons plus loin comment se prémunir d'un changement inattendu.
@ -126,35 +112,32 @@ Dans les étapes ci-dessous, nous épinglerons une version LTS afin de nous assu
\section{Django} \section{Django}
Comme nous l'avons vu ci-dessus, \texttt{django-admin} permet de créer un nouveau projet. 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}: Nous faisons ici une distinction entre un \textbf{projet} et une \textbf{application}:
\begin{itemize} \begin{itemize}
\item \item
\textbf{Un projet} représente l'ensemble des applications, paramètres, middlewares, dépendances, ..., qui font que votre code fait ce qu'il est sensé faire. \textbf{Un projet} représente l'ensemble des applications, paramètres, middlewares, dépendances, ..., qui font que votre code fait ce qu'il est sensé faire.
Il s'agit grosso modo d'un câblage de tous les composants entre eux. Il s'agit grosso modo d'un câblage de tous les composants entre eux.
\item \item
\textbf{Une application} est un contexte d'exécution (vues, comportements, pages HTML, ...), idéalement autonome, d'une partie du projet. \textbf{Une application} est un contexte d'exécution (vues, comportements, pages HTML, ...), idéalement autonome, d'une partie du projet.
Une application est supposée avoir une portée de réutilisation, même s'il ne sera pas toujours possible de viser une généricité parfaite. Une application est supposée avoir une portée de réutilisation, même s'il ne sera pas toujours possible de viser une généricité parfaite.
\end{itemize} \end{itemize}
Pour \texttt{gwift}, nous aurons: Pour \texttt{gwift}, nous aurons :
\begin{figure} \begin{figure}
\centering \centering
\includegraphics{images/django/django-project-vs-apps-gwift.png} \includegraphics{images/django/django-project-vs-apps-gwift.png}
\caption{Projet Django vs Applications} \caption{Projet Django vs Applications}
\end{figure} \end{figure}
\begin{enumerate} \begin{enumerate}
\item \item
Une première application pour la gestion des listes de souhaits et des Une première application pour la gestion des listes de souhaits et des éléments,
éléments, \item
\item Une deuxième application pour la gestion des utilisateurs,
Une deuxième application pour la gestion des utilisateurs, \item
\item Voire une troisième application qui gérera les partages entre utilisateurs et listes.
Voire une troisième application qui gérera les partages entre
utilisateurs et listes.
\end{enumerate} \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 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
@ -168,8 +151,8 @@ Pour \texttt{khana}, nous pourrions avoir quelque chose comme ceci:
\caption{Django Project vs Applications} \caption{Django Project vs Applications}
\end{figure} \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. 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 projets. Ceci pourrait être commun aux deux projets.
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. 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: 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}, ... 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}, ...
@ -179,8 +162,7 @@ Le projet s'occupe principalement d'appliquer une couche de glue entre différen
\subsection{manage.py} \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}. 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 A partir de maintenant, nous n'utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet :
ce qui touchera à la gestion de notre projet:
\begin{itemize} \begin{itemize}
\item \item
@ -204,7 +186,7 @@ catégories:
\item \item
\textbf{auth}: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant. \textbf{auth}: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
\item \item
\textbf{django}: vérifier la \textbf{conformité} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, ... \textbf{django}: vérifier la \textbf{conformité} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, ...
Ce sont des commandes d'administration générale. Ce sont des commandes d'administration générale.
\item \item
\textbf{sessions}: suppressions des sessions en cours \textbf{sessions}: suppressions des sessions en cours
@ -240,7 +222,7 @@ Si nous nous rendons sur la page \url{http://127.0.0.1:8000} (ou \url{http://loc
\caption{python manage.py runserver (Non, ce n'est pas Challenger)} \caption{python manage.py runserver (Non, ce n'est pas Challenger)}
\end{figure} \end{figure}
Nous avons mis un morceau de la sortie console entre crochet \texttt{{[}\ldots{}\hspace{0pt}{]}} ci-dessus, car elle concerne les migrations. 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.} 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. Cela concerne les migrations, et c'est un point que nous verrons un peu plus tard.
@ -261,8 +243,8 @@ Résultat? Django nous a créé un répertoire \texttt{wish}, dans lequel nous t
\item \item
\texttt{wish/\_\_init\_\_.py} pour que notre répertoire \texttt{wish} soit converti en package Python. \texttt{wish/\_\_init\_\_.py} pour que notre répertoire \texttt{wish} soit converti en package Python.
\item \item
\texttt{wish/admin.py} servira à structurer l'administration de notre application. \texttt{wish/admin.py} servira à structurer l'administration de notre application.
Chaque information peut être gérée facilement au travers d'une interface générée à la volée par le framework. Chaque information peut être gérée facilement au travers d'une interface générée à la volée par le framework.
Nous y reviendrons par la suite. Nous y reviendrons par la suite.
\item \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/} \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/}
@ -274,7 +256,7 @@ Résultat? Django nous a créé un répertoire \texttt{wish}, dans lequel nous t
\texttt{wish/tests.py} pour les tests unitaires. \texttt{wish/tests.py} pour les tests unitaires.
\end{itemize} \end{itemize}
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire \texttt{wish} dans votre répertoire \texttt{gwift} existant. Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire \texttt{wish} dans votre répertoire \texttt{gwift} existant.
C'est \emph{une} forme de convention. C'est \emph{une} forme de convention.
La structure de vos répertoires devient celle-ci: La structure de vos répertoires devient celle-ci:
@ -286,19 +268,19 @@ Notre application a bien été créée, et nous l'avons déplacée dans le répe
\section{Fonctionnement général} \section{Fonctionnement général}
Le métier de programmeur est devenu de plus en plus complexe. 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 actions à réaliser: requêtes en bases de données, construction de la page, ... Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actions à réaliser : requêtes en bases de données, construction de la page, ...
La recherche d'une solution à 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. La recherche d'une solution à 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 frameworks) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ repeat\ yourself} \footnote{DRY}. Django (et d'autres frameworks) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ 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). 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. Le chemin parcouru par une requête est expliqué en (petits) détails ci-dessous.
\textbf{Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application}. \textbf{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}. 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}. 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. Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu'elle va parcourir.
\begin{figure} \begin{figure}
@ -314,7 +296,7 @@ Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chem
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path
from gwift.views import wish_details from gwift.views import wish_details
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details), path("wishes/<int:wish_id>", wish_details),
@ -323,7 +305,7 @@ Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chem
\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 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. \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 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}. 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}.
@ -335,17 +317,17 @@ niveau de la ligne \texttt{path("wishes/\textless{}int:wish\_id\textgreater{}} d
Champomy et cotillons! Nous avons une correspondance avec Champomy et cotillons! Nous avons une correspondance avec
\texttt{wishes/details/91827} \texttt{wishes/details/91827}
\end{itemize} \end{itemize}
Le module \texttt{gwift.views} qui se trouve dans le fichier \texttt{gwift/views.py} peut ressembler à ceci: Le module \texttt{gwift.views} qui se trouve dans le fichier \texttt{gwift/views.py} peut ressembler à ceci:
\begin{minted}{Python} \begin{minted}{Python}
# gwift/views.py # gwift/views.py
[...] [...]
from datetime import datetime from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse: def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = { context = {
"user_name": "Bond," "user_name": "Bond,"
@ -364,7 +346,7 @@ Pour résumer, cette fonction permet:
\begin{enumerate} \begin{enumerate}
\item \item
De construire un \emph{contexte}, qui est représenté sous la forme d'un dictionnaire associant des clés à des valeurs. 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}. 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 \item
Nous passons ensuite ce dictionnaire à un canevas, \texttt{wish\_details.html}, que l'on trouve normalement dans le répertoire \texttt{templates} de notre projet, ou dans le répertoire \texttt{templates} propre à notre application. Nous passons ensuite ce dictionnaire à un canevas, \texttt{wish\_details.html}, que l'on trouve normalement dans le répertoire \texttt{templates} de notre projet, ou dans le répertoire \texttt{templates} propre à notre application.
@ -413,7 +395,7 @@ Après application de notre contexte sur ce template, nous obtiendrons ce docume
\subsection{Structure finale} \subsection{Structure finale}
En repartant de la structure initiale décrite au chapitre précédent, nous arrivons à ceci. En repartant de la structure initiale décrite au chapitre précédent, nous arrivons à ceci.
TODO : passer à poetry TODO : passer à poetry
@ -422,9 +404,9 @@ TODO : passer à poetry
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 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, ... 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. 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 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. 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à}. 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): Pour démarrer, créez un environnement virtuel (comme d'habitude):
@ -432,7 +414,7 @@ Pour démarrer, créez un environnement virtuel (comme d'habitude):
\begin{verbatim} \begin{verbatim}
python -m venv .venvs\cookie-cutter-khana python -m venv .venvs\cookie-cutter-khana
.venvs\cookie-cutter-khana\Scripts\activate.bat .venvs\cookie-cutter-khana\Scripts\activate.bat
(cookie-cutter-khana) $ pip install cookiecutter (cookie-cutter-khana) $ pip install cookiecutter
Collecting cookiecutter Collecting cookiecutter
[...] [...]
@ -446,11 +428,11 @@ Pour démarrer, créez un environnement virtuel (comme d'habitude):
[SUCCESS]: Project initialized, keep up the good work! [SUCCESS]: Project initialized, keep up the good work!
\end{verbatim} \end{verbatim}
Si vous explorez les différents fichiers, vous trouverez beaucoup de similitudes avec la configuration que nous vous proposions ci-dessus. 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. 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 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. existant.
La \href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startproject}{documentation} à ce sujet est assez complète. La \href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startproject}{documentation} à ce sujet est assez complète.
\begin{verbatim} \begin{verbatim}
@ -459,12 +441,12 @@ La \href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startprojec
\section{Tests unitaires} \section{Tests unitaires}
Il y a deux manières d'écrire les tests: soit avant, soit après l'implémentation. Il y a deux manières d'écrire les tests: soit avant, soit après l'implémentation.
Oui, idéalement, les tests doivent être écrits à l'avance. Entre nous, on ne va pas râler si vous faites l'inverse, l'important étant que vous le fassiez. Une bonne métrique pour vérifier l'avancement des tests est la couverture de code. Oui, idéalement, les tests doivent être écrits à l'avance. Entre nous, on ne va pas râler si vous faites l'inverse, l'important étant que vous le fassiez. Une bonne métrique pour vérifier l'avancement des tests est la couverture de code.
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}: 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: On a deux choix ici:
\begin{enumerate} \begin{enumerate}
\item Utiliser les librairies de test de Django \item Utiliser les librairies de test de Django
@ -486,13 +468,13 @@ On a deux choix ici:
\subsection{Couverture de code} \subsection{Couverture de code}
Quel que soit le framework de tests choisi (django-tests, pytest, unittest, ...), la couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Quel que soit le framework de tests choisi (django-tests, pytest, unittest, ...), la couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests.
Il ne s'agit pas de vérifier que le code est bien testé, mais de vérifier quelle partie du code est testée. Il ne s'agit pas de vérifier que le code est bien testé, mais de vérifier quelle partie du code est testée.
Le paquet coverage se charge dévaluer le pourcentage de code couvert par les tests. Le paquet coverage se charge dévaluer le pourcentage de code couvert par les tests.
Avec pytest, il convient dutiliser le paquet pytest-cov, suivi de la commande pytest Avec pytest, il convient dutiliser le paquet pytest-cov, suivi de la commande pytest
\texttt{--cov=gwift tests/}. \texttt{--cov=gwift tests/}.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer par le paquet django-coverage-plugin. Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer par le paquet django-coverage-plugin.
Ajoutez-le dans le fichier requirements/base.txt, et lancez une couverture de code grâce à la commande coverage. Ajoutez-le dans le fichier requirements/base.txt, et lancez une couverture de code grâce à la commande coverage.
La configuration peut se faire dans un fichier .coveragerc que vous placerez à la racine de votre projet, et qui sera lu lors de lexécution. La configuration peut se faire dans un fichier .coveragerc que vous placerez à la racine de votre projet, et qui sera lu lors de lexécution.
@ -509,10 +491,10 @@ La configuration peut se faire dans un fichier .coveragerc que vous placerez à
omit = ../*migrations* omit = ../*migrations*
plugins = plugins =
django_coverage_plugin django_coverage_plugin
[report] [report]
ignore_errors = True ignore_errors = True
[html] [html]
directory = coverage_html_report directory = coverage_html_report
\end{verbatim} \end{verbatim}
@ -574,9 +556,9 @@ class HomeTests(TestCase):
\begin{minted}{python} \begin{minted}{python}
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from .views import home from .views import home
class HomeTests(TestCase): class HomeTests(TestCase):
def test_home_view_status_code(self): def test_home_view_status_code(self):
view = resolve("/") view = resolve("/")
@ -599,7 +581,7 @@ class Wish(models.Model):
total = self.number_of_parts * self.numbers_available total = self.number_of_parts * self.numbers_available
percentage = (number_of_linked_parts / total) percentage = (number_of_linked_parts / total)
return percentage * 100 return percentage * 100
\end{minted} \end{minted}
Lancez maintenant la couverture de code. Vous obtiendrez ceci: Lancez maintenant la couverture de code. Vous obtiendrez ceci:
@ -643,7 +625,7 @@ class TestWishModel(TestCase):
description='This is a faked wishlist' description='This is a faked wishlist'
) )
wishlist.save() wishlist.save()
wish = Wish( wish = Wish(
wishlist=wishlist, wishlist=wishlist,
name='Fake Wish', name='Fake Wish',
@ -651,25 +633,25 @@ class TestWishModel(TestCase):
number_of_parts=4 number_of_parts=4
) )
wish.save() wish.save()
part1 = WishPart(wish=wish, comment='part1') part1 = WishPart(wish=wish, comment='part1')
part1.save() part1.save()
self.assertEqual(25, wish.percentage_of_completion) self.assertEqual(25, wish.percentage_of_completion)
part2 = WishPart(wish=wish, comment='part2') part2 = WishPart(wish=wish, comment='part2')
part2.save() part2.save()
self.assertEqual(50, wish.percentage_of_completion) self.assertEqual(50, wish.percentage_of_completion)
part3 = WishPart(wish=wish, comment='part3') part3 = WishPart(wish=wish, comment='part3')
part3.save() part3.save()
self.assertEqual(75, wish.percentage_of_completion) self.assertEqual(75, wish.percentage_of_completion)
part4 = WishPart(wish=wish, comment='part4') part4 = WishPart(wish=wish, comment='part4')
part4.save() part4.save()
self.assertEqual(100, wish.percentage_of_completion) self.assertEqual(100, wish.percentage_of_completion)
\end{minted} \end{minted}
@ -685,7 +667,7 @@ En relançant la couverture de code, on voit à présent que nous arrivons à 99
. .
---------------------------------------------------------------------- ----------------------------------------------------------------------
Ran 1 test in 0.006s Ran 1 test in 0.006s
OK OK
Creating test database for alias 'default'... Creating test database for alias 'default'...
Destroying test database for alias 'default'... Destroying test database for alias 'default'...
@ -703,7 +685,7 @@ En relançant la couverture de code, on voit à présent que nous arrivons à 99
------------------------------------------------------------------ ------------------------------------------------------------------
TOTAL 87 0 4 1 99% TOTAL 87 0 4 1 99%
\end{verbatim} \end{verbatim}
En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code. En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code.
A noter que tester le modèle en lui-même (ses attributs ou champs) ou des composants internes à Django n'a pas de sens: cela reviendrait à mettre en doute son fonctionnement interne. A noter que tester le modèle en lui-même (ses attributs ou champs) ou des composants internes à Django n'a pas de sens: cela reviendrait à mettre en doute son fonctionnement interne.

View File

@ -28,29 +28,20 @@ En fonction de votre niveau d'apprentissage du langage, plusieurs
ressources pourraient vous aider: ressources pourraient vous aider:
\begin{itemize} \begin{itemize}
\item \item
\textbf{Pour les débutants}, \textbf{Pour les débutants}, \href{https://automatetheboringstuff.com/}{Automate the Boring Stuff with Python} \cite{boring_stuff}, aka. \emph{Practical Programming for Total Beginners}
\href{https://automatetheboringstuff.com/}{Automate the Boring Stuff \item
with Python} \cite{boring_stuff}, aka. \emph{Practical \textbf{Pour un (gros) niveau au dessus} et pour un état de l'art du langage, nous ne pouvons que vous recommander le livre Expert Python Programming \cite{expert_python}, qui aborde énormément d'aspects du langage en détails (mais pas toujours en profondeur): les différents types d'interpréteurs, les éléments de langage avancés, différents outils de productivité, métaprogrammation, optimisation de code, programmation orientée évènements, multithreading et concurrence, tests, \ldots
Programming for Total Beginners} A ce jour, c'est le concentré de sujets liés au langage le plus intéressant qui ait pu arriver entre nos mains.
\item
\textbf{Pour un (gros) niveau au dessus} et pour un état de l'art du langage, nous ne pouvons que vous recommander le livre Expert Python Programming \cite{expert_python}, qui aborde énormément d'aspects du langage en détails (mais pas toujours en profondeur): les différents types d'interpréteurs, les éléments de langage avancés, différents outils de productivité, métaprogrammation, optimisation de code, programmation orientée évènements, multithreading et concurrence, tests, ...
A ce jour, c'est le concentré de sujets liés au langage le plus intéressant qui ait pu arriver entre nos mains.
\end{itemize} \end{itemize}
En parallèle, si vous avez besoin d'un aide-mémoire ou d'une liste En parallèle, si vous avez besoin d'un aide-mémoire ou d'une liste exhaustive des types et structures de données du langage, référez-vous au lien suivant: \href{https://gto76.github.io/python-cheatsheet/}{Python Cheat Sheet}.
exhaustive des types et structures de données du langage, référez-vous
au lien suivant:
\href{https://gto76.github.io/python-cheatsheet/}{Python Cheat Sheet}.
\section{Protocoles de langage} \section{Protocoles de langage}
Le modèle de données du langage spécifie un ensemble de méthodes qui Le modèle de données du langage spécifie un ensemble de méthodes qui peuvent être surchargées.
peuvent être surchargées. Ces méthodes suivent une convention de nommage Ces méthodes suivent une convention de nommage et leur nom est toujours encadré par un double tiret souligné; d'où leur nom de "\emph{dunder methods}\index{dunder}" ou "\emph{double-underscore methods}".
et leur nom est toujours encadré par un double tiret souligné; d'où leur La méthode la plus couramment utilisée est la méthode \texttt{init()}, qui permet de surcharger l'initialisation d'une instance de classe.
nom de "\emph{dunder methods}\index{dunder}" ou "\emph{double-underscore methods}". La
méthode la plus couramment utilisée est la méthode \texttt{init()}, qui
permet de surcharger l'initialisation d'une instance de classe.
\begin{listing}[!ht] \begin{listing}[!ht]
\begin{minted}{python} \begin{minted}{python}
@ -70,9 +61,9 @@ Vous trouverez ci-dessous un tableau reprenant les protocoles les plus courants:
\begin{center} \begin{center}
\begin{tabular}{ c c c } \begin{tabular}{ c c c }
cell1 & cell2 & cell3 \\ cell1 & cell2 & cell3 \\
cell4 & cell5 & cell6 \\ cell4 & cell5 & cell6 \\
cell7 & cell8 & cell9 cell7 & cell8 & cell9
\end{tabular} \end{tabular}
\end{center} \end{center}
@ -83,7 +74,7 @@ Les points principaux à présenter ci-dessus:
\item item() \item item()
\item getitem() - pour utiliser les [] \item getitem() - pour utiliser les []
\item len() - pour connaître la longueur d'un objet \item len() - pour connaître la longueur d'un objet
\item ... \item \ldots
\end{enumerate} \end{enumerate}
Le langage autorise nativement plus d'une cinquantaine d'opérateurs différents: Le langage autorise nativement plus d'une cinquantaine d'opérateurs différents:
@ -94,7 +85,7 @@ Le langage autorise nativement plus d'une cinquantaine d'opérateurs différents
\item Opérateurs de comparaisons \item Opérateurs de comparaisons
\item Opérateurs d'identité \item Opérateurs d'identité
\item Opérateurs de comparaison bit à bit \item Opérateurs de comparaison bit à bit
\item ... \item \ldots
\end{enumerate} \end{enumerate}
Par exemple, la méthode \texttt{add()} est responsable de l'implémentation de l'opérateur \texttt{+}, la méthode \texttt{sub()} s'occupe de la soustraction ()\texttt{}), tandis que \texttt{mul()} gère l'opérateur \texttt{*}. Par exemple, la méthode \texttt{add()} est responsable de l'implémentation de l'opérateur \texttt{+}, la méthode \texttt{sub()} s'occupe de la soustraction ()\texttt{}), tandis que \texttt{mul()} gère l'opérateur \texttt{*}.
@ -156,11 +147,11 @@ Nous pouvons donc utiliser ces mêmes \textbf{dunder methods} (\textbf{double-un
\section{Guide de style} \section{Guide de style}
La première PEP qui va nous intéresser est la PEP8. La première PEP qui va nous intéresser est la PEP8.
Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, ... Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, \ldots
En bref, elle décrit comment écrire du code proprement, afin que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité. En bref, elle décrit comment écrire du code proprement, afin que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Dans cet objectif, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: flake8. Pour l'installer, passez par pip. Dans cet objectif, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: flake8. Pour l'installer, passez par pip.
Lancez ensuite la commande \texttt{flake8} suivie du chemin à analyser (\texttt{.}, le nom d'un répertoire, le nom d'un fichier \texttt{.py}, ...). Lancez ensuite la commande \texttt{flake8} suivie du chemin à analyser (\texttt{.}, le nom d'un répertoire, le nom d'un fichier \texttt{.py}, \ldots).
Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options \texttt{-\/-statistics\ -qq} - l'attribut \texttt{-qq} permettant simplement d'ignorer toute sortie console autre que les statistiques demandées). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options \texttt{-\/-statistics\ -qq} - l'attribut \texttt{-qq} permettant simplement d'ignorer toute sortie console autre que les statistiques demandées).
\begin{listing}[!ht] \begin{listing}[!ht]
@ -196,12 +187,12 @@ Toute fonction dans la complexité est supérieure à cette valeur sera considé
Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires que nous verrons plus bas, de documenter son code. Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires que nous verrons plus bas, de documenter son code.
Ceci impose une certaine rigueur, tout en améliorant énormément la qualité, la compréhension et la reprise du code par une tierce personne. Ceci impose une certaine rigueur, tout en améliorant énormément la qualité, la compréhension et la reprise du code par une tierce personne.
Ceci implique aussi de \textbf{tout} documenter: les modules, les paquets, les classes, les fonctions, méthodes, ... ce qui peut aller à contrecourant d'autres pratiques \cite{clean_code}{53-74} ; il y a donc une juste mesure à prendre entre "tout documenter" et "tout bien documenter": Ceci implique aussi de \textbf{tout} documenter: les modules, les paquets, les classes, les fonctions, méthodes, \ldots ce qui peut aller à contrecourant d'autres pratiques \cite{clean_code}{53-74} ; il y a donc une juste mesure à prendre entre "tout documenter" et "tout bien documenter":
\begin{itemize} \begin{itemize}
\item \item
Inutile d'ajouter des watermarks, auteurs, ... Inutile d'ajouter des watermarks, auteurs, \ldots
Git ou tout VCS s'en sortira très bien et sera beaucoup plus efficace que n'importe quelle chaîne de caractères que vous pourriez indiquer et qui sera fausse dans six mois, Git ou tout VCS s'en sortira très bien et sera beaucoup plus efficace que n'importe quelle chaîne de caractères que vous pourriez indiquer et qui sera fausse dans six mois,
\item \item
Inutile de décrire quelque chose qui est évident; documenter la méthode \mintinline{python}{get_age()} d'une personne n'aura pas beaucoup d'intérêt Inutile de décrire quelque chose qui est évident; documenter la méthode \mintinline{python}{get_age()} d'une personne n'aura pas beaucoup d'intérêt
@ -219,7 +210,7 @@ Il existe plusieurs types de balisages reconnus/approuvés:
\item Google Style (parfois connue sous l'intitulé \texttt{Napoleon}) \item Google Style (parfois connue sous l'intitulé \texttt{Napoleon})
\end{enumerate} \end{enumerate}
... mais tout système de balisage peut être reconnu, sous réseve de respecter la structure de la PEP257. \ldots mais tout système de balisage peut être reconnu, sous réseve de respecter la structure de la PEP257.
\subsection{PEP 257} \subsection{PEP 257}
@ -234,7 +225,7 @@ Elle contient des conventions, pas des règles ou
-- Tim Peters on comp.lang.python, 2001-06-16 -- Tim Peters on comp.lang.python, 2001-06-16
\end{quote} \end{quote}
Ainsi, les conventions sont décrites; chaque format propose ensuite son propre balisage (ReStructuredText, Numpy, Napoleon, ...). Ainsi, les conventions sont décrites; chaque format propose ensuite son propre balisage (ReStructuredText, Numpy, Napoleon, \ldots).
A priori, vous pourriez tout à fait utiliser le vôtre, sous réserve que les conventions de la PEP-257 soient respectées. A priori, vous pourriez tout à fait utiliser le vôtre, sous réserve que les conventions de la PEP-257 soient respectées.
\subsection{RestructuredText} \subsection{RestructuredText}
@ -483,7 +474,7 @@ Cet élément peut être:
\begin{enumerate} \begin{enumerate}
\item \textbf{Une ligne de code} \item \textbf{Une ligne de code}
\item \textbf{Un bloc de code} - une fonction, une méthode, une classe, un module, ... \item \textbf{Un bloc de code} - une fonction, une méthode, une classe, un module, \ldots
\item \textbf{Un projet entier}, en spécifiant la non-prise en compte au niveau du fichier \texttt{.pylintrc}, qui contient \item \textbf{Un projet entier}, en spécifiant la non-prise en compte au niveau du fichier \texttt{.pylintrc}, qui contient
\end{enumerate} \end{enumerate}
@ -496,7 +487,7 @@ Cet élément peut être:
\section{Formatage de code} \section{Formatage de code}
Nous avons parlé ci-dessous de style de codage pour Python (PEP8), de style de rédaction pour la documentation (PEP257), d'un vérificateur pour nous indiquer quels morceaux de code doivent absolument être revus, ... Nous avons parlé ci-dessous de style de codage pour Python (PEP8), de style de rédaction pour la documentation (PEP257), d'un vérificateur pour nous indiquer quels morceaux de code doivent absolument être revus, \ldots
Reste que ces tâches sont parfois (très) souvent fastidieuses: écrire un code propre et systématiquement cohérent est une tâche ardue. Reste que ces tâches sont parfois (très) souvent fastidieuses: écrire un code propre et systématiquement cohérent est une tâche ardue.
Heureusement, il existe plusieurs outils pour nous aider au niveau du formatage automatique. Heureusement, il existe plusieurs outils pour nous aider au niveau du formatage automatique.
Même si elle n'est pas parfaite, la librairie \href{https://black.readthedocs.io/en/stable/}{Black} arrive à un très bon compromis entre Même si elle n'est pas parfaite, la librairie \href{https://black.readthedocs.io/en/stable/}{Black} arrive à un très bon compromis entre

View File

@ -2,70 +2,74 @@
\chapter{Tests unitaires et d'intégration} \chapter{Tests unitaires et d'intégration}
\begin{quote} \begin{quote}
Tests are part of the system. Tests are part of the system.
You can think of tests as the outermost circle in the architecture. You can think of tests as the outermost circle in the architecture.
Nothing within in the system depends on the tests, and the tests always depend inward on the components of the system. Nothing within in the system depends on the tests, and the tests always depend inward on the components of the system.
-- Robert C. Martin, Clean Architecture -- Robert C. Martin, Clean Architecture
\end{quote} \end{quote}
Your tests are your first and best line of defense against software defects. Your tests are more important than linting \& static analysis (which can only find a subclass of errors, not problems with your actual program logic). Tests are as important as the implementation itself (all that matters is that the code meets the requirement -- how it's implemented doesn't matter at all unless it's implemented poorly). Your tests are your first and best line of defense against software defects.
Your tests are more important than linting \& static analysis (which can only find a subclass of errors, not problems with your actual program logic).
Tests are as important as the implementation itself (all that matters is that the code meets the requirement -- how it's implemented doesn't matter at all unless it's implemented poorly).
Unit tests combine many features that make them your secret weapon to application success: Unit tests combine many features that make them your secret weapon to application success:
\begin{enumerate} \begin{enumerate}
\item \item
Design aid: Writing tests first gives you a clearer perspective on the ideal API design. Design aid: Writing tests first gives you a clearer perspective on the ideal API design.
\item \item
Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement. Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.
\item \item
Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements? Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?
\item \item
Quality Assurance: Manual QA is error prone. In my experience, it's impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features. Quality Assurance: Manual QA is error prone.
\item In my experience, it's impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features.
Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production. \item
Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.
\end{enumerate} \end{enumerate}
Unit tests don't need to be twisted or manipulated to serve all of those broad-ranging goals. Rather, it is in the essential nature of a unit test to satisfy all of those needs. These benefits are all side-effects Unit tests don't need to be twisted or manipulated to serve all of those broad-ranging goals.
of a well-written test suite with good coverage. Rather, it is in the essential nature of a unit test to satisfy all of those needs.
These benefits are all side-effects of a well-written test suite with good coverage.
\begin{enumerate} \begin{enumerate}
\item \item
What component aspect are you testing? What component aspect are you testing?
\item \item
What should the feature do? What specific behavior requirement are you testing? What should the feature do? What specific behavior requirement are you testing?
\end{enumerate} \end{enumerate}
Traduit grossièrement depuis un article sur \url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_}: Traduit grossièrement depuis un article sur \url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_}:
% TODO : Finir le verbatim ci-dessous : "ils sont.... ??????"
\begin{verbatim} \begin{verbatim}
Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont
\end{verbatim} \end{verbatim}
\begin{verbatim} \begin{verbatim}
Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi: Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:
\end{verbatim} \end{verbatim}
\begin{enumerate} \begin{enumerate}
\item \item
Aide au design: écrire des tests avant d'écrire le code vous donnera Aide au design: écrire des tests avant d'écrire le code vous donnera une meilleure perspective sur le design à appliquer aux API.
une meilleure perspective sur le design à appliquer aux API. \item
\item Documentation (pour les développeurs): chaque description d'un test
Documentation (pour les développeurs): chaque description d'un test \item
\item Tester votre compréhension en tant que développeur:
Tester votre compréhension en tant que développeur: \item
\item Assurance qualité: des tests, 5.
Assurance qualité: des tests, 5.
\end{enumerate} \end{enumerate}
\section{Complexité cyclomatique\index{McCabe}} \section{Complexité cyclomatique\index{McCabe}}
La \href{https://fr.wikipedia.org/wiki/Nombre_cyclomatique}{complexité cyclomatique} (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. La \href{https://fr.wikipedia.org/wiki/Nombre_cyclomatique}{complexité cyclomatique} (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section.
Quand le cycle d'exécution du code rencontre une condition, cette condition peut être évalue à VRAI ou à FAUX. Quand le cycle d'exécution du code rencontre une condition, cette condition peut être évalue à VRAI ou à FAUX.
L'exécution du code dispose donc de deux embranchements, correspondant chacun à un résultat de cette condition. L'exécution du code dispose donc de deux embranchements, correspondant chacun à un résultat de cette condition.
Le code suivant \autoref{cyclomatic-simple-code} a une complexité cyclomatique 1; il s'agit du cas le plus simple que nous pouvons implémenter: l'exécution du code rentre dans la fonction (il y a un seul embranchement), et aucun bloc conditionnel n'est présent sur son chemin. Le code suivant \autoref{cyclomatic-simple-code} a une complexité cyclomatique 1; il s'agit du cas le plus simple que nous pouvons implémenter : l'exécution du code rentre dans la fonction (il y a un seul embranchement), et aucun bloc conditionnel n'est présent sur son chemin.
La complexité reste de 1. La complexité reste de 1.
\begin{listing}[!hbpt] \begin{listing}[!hbpt]
@ -93,18 +97,21 @@ Si nous complexifions cette fonction en vérifiant (par exemple) le jour de la s
\caption{Ajout d'une fonctionnalité essentielle et totalement indispensable} \caption{Ajout d'une fonctionnalité essentielle et totalement indispensable}
\end{listing} \end{listing}
La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1.
Si nous rencontrons une condition, elle passera à 2, etc. Si nous rencontrons une condition, elle passera à 2, etc.
Cette complexité est liée à deux concepts: Cette complexité est liée à deux concepts:
\begin{itemize} \begin{itemize}
\item \textbf{La lisibilité du code}: au plus la complexité cyclomatique sera élevée, au plus le code sera compliqué à comprendre en première instance. Il sera composé de plusieurs conditions, éventuellement imbriquées, il débordera probablement de la hauteur que votre écran sera capable d'afficher \item
\item \textbf{Les tests unitaires}: pour nous assurer d'une couverture de code correcte, il sera nécessaire de couvrir tous les embranchements présentés. Connaître la complexité permet de savoir combien de tests devront être écrits pour assurer une couverture complète de tous les cas pouvant se présenter. \textbf{La lisibilité du code}: au plus la complexité cyclomatique sera élevée, au plus le code sera compliqué à comprendre en première instance. Il sera composé de plusieurs conditions, éventuellement imbriquées, il débordera probablement de la hauteur que votre écran sera capable d'afficher
\item
\textbf{Les tests unitaires}: pour nous assurer d'une couverture de code correcte, il sera nécessaire de couvrir tous les embranchements présentés. Connaître la complexité permet de savoir combien de tests devront être écrits pour assurer une couverture complète de tous les cas pouvant se présenter.
\end{itemize} \end{itemize}
\section{Lisibilité du code} \section{Lisibilité du code}
Il est important de noter que refactoriser un bloc, par exemple en extrayant une méthode, n'améliore pas la complexité cyclomatique globale de l'application. Il est important de noter que refactoriser un bloc, par exemple en extrayant une méthode, n'améliore pas la complexité cyclomatique globale de l'application.
L'amélioration que nous visons ici est une amélioration \textbf{locale}, qui facilite la lecture d'un bloc spécifique, et pas d'un programme complet. L'amélioration que nous visons ici est une amélioration \textbf{locale}, qui facilite la lecture d'un bloc spécifique, et pas d'un programme complet.
"Améliorons" notre code ci-dessous, pour lui ajouter la possibilité de gérer les autres jours de la semaine: "Améliorons" notre code ci-dessous, pour lui ajouter la possibilité de gérer les autres jours de la semaine:
@ -134,7 +141,7 @@ L'amélioration que nous visons ici est une amélioration \textbf{locale}, qui f
\label{Impression du jour de la semaine, version naïve} \label{Impression du jour de la semaine, version naïve}
\end{listing} \end{listing}
La complexité de ce code est évaluée à 8, même si la complexité effective ne sera que de 7. La complexité de ce code est évaluée à 8, même si la complexité effective ne sera que de 7.
Extraire une méthode à partir de ce bloc pourra réduire la complexité de la fonction \mintinline{python}{print_current_date} n'améliorera rien et ne fera que déplacer le problème. Extraire une méthode à partir de ce bloc pourra réduire la complexité de la fonction \mintinline{python}{print_current_date} n'améliorera rien et ne fera que déplacer le problème.
Une solution serait de passer par un dictionnaire, de façon à ramener la complexité à 1: Une solution serait de passer par un dictionnaire, de façon à ramener la complexité à 1:
@ -161,20 +168,20 @@ Une solution serait de passer par un dictionnaire, de façon à ramener la compl
\section{Types de tests} \section{Types de tests}
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, cest sans doute que larchitecture de la solution nest pas adaptée et que les composants sont couplés les uns aux autres. 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, cest sans doute que larchitecture de la solution nest 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é 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} 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 lautomatisant au plus près de sa source de création: Le plus important est de toujours corréler les phases de tests indépendantes du reste du travail (de développement, ici), en lautomatisant au plus près de sa source de création:
\begin{quote} \begin{quote}
Martin Fowler observes that, in general, "a ten minute build [and test process] is perfectly within reason... 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. [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, wont be found. However any bugs that involve larger scale intercations, particularly those involving the real database, wont 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. 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. This suite may take a couple of hours to run.
-- Robert C. Martin, Clean Architecture -- Robert C. Martin, Clean Architecture
\end{quote} \end{quote}
\subsection{Tests unitaires} \subsection{Tests unitaires}
@ -188,21 +195,19 @@ Les tests unitaires ciblent typiquement une seule fonction, classe ou méthode,
Pour plusieurs raisons (et notamment en raison de performances), les tests unitaires utilisent souvent des données stubbées - pour éviter dappeler le "vrai" service Pour plusieurs raisons (et notamment en raison de performances), les tests unitaires utilisent souvent des données stubbées - pour éviter dappeler 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. 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. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil.
Certains recommandent de le garder sous une complexité de 10; d'autres de 5. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants.
Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante.
Il existe plusieurs types de tests (intégration, comportement, ...) Il existe plusieurs types de tests (intégration, comportement, ...)
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. Avoir des tests, c'est bien.
S'assurer que tout est testé, c'est mieux.
C'est ici qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré, le but du jeu consistant simplement à augmenter ou égaler le pourcentage de couverture de code existant avant chaque modification. C'est ici qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré, le but du jeu consistant simplement à augmenter ou égaler le pourcentage de couverture de code existant avant chaque modification.
Gitlab permet de visualiser cette information de manière très propre, en l'affichant au niveau de chaque proposition d'intégration. Gitlab permet de visualiser cette information de manière très propre, en l'affichant au niveau de chaque proposition d'intégration.
La couverture de code est une analyse qui donne un pourcentage lié à la La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est \textbf{bien} testé, mais juste de vérifier \textbf{quelle partie} du code est testée.
quantité de code couvert par les tests. Attention qu'il ne s'agit pas de Le paquet \texttt{coverage} se charge d'évaluer le pourcentage de code couvert par les tests.
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} \subsection{Tests d'acceptance}
@ -211,9 +216,7 @@ se charge d'évaluer le pourcentage de code couvert par les tests.
what the customer meant it to. what the customer meant it to.
\end{quote} \end{quote}
Les tests dacceptance vérifient que lapplication fonctionne comme convenu, mais à un Les tests dacceptance vérifient que lapplication fonctionne comme convenu, mais à un plus haut niveau (fonctionnement correct dune API, validation dune chaîne dactions effectuées par un humain, ...).
plus haut niveau (fonctionnement correct dune API, validation dune chaîne dactions
effectuées par un humain, ...).
\subsection{Tests d'intégration} \subsection{Tests d'intégration}

View File

@ -2,29 +2,25 @@
\section{Environnement de développement intégré \index{IDE}} \section{Environnement de développement intégré \index{IDE}}
Concrètement, nous pourrions tout à fait nous limiter à Notepad ou Notepad++. Concrètement, nous pourrions tout à fait nous limiter à Notepad ou Notepad++.
Mais à moins d'aimer se fouetter avec un câble USB, nous apprécions la complétion du code, la coloration syntaxique, l'intégration des tests unitaires et d'un debugger, ainsi que deux-trois sucreries qui feront plaisir à n'importe quel développeur. Mais à moins d'aimer se fouetter avec un câble USB, nous apprécions la complétion du code, la coloration syntaxique, l'intégration des tests unitaires et d'un debugger, ainsi que deux-trois sucreries qui feront plaisir à n'importe quel développeur.
Si vous manquez d'idées ou si vous ne savez pas par où commencer: Si vous manquez d'idées ou si vous ne savez pas par où commencer:
\begin{itemize} \begin{itemize}
\item \item
\href{https://vscodium.com/}{VSCodium}, avec les plugins \href{https://vscodium.com/}{VSCodium}, avec les plugins
\href{https://marketplace.visualstudio.com/items?itemName=ms-python.python}{Python}et \href{https://marketplace.visualstudio.com/items?itemName=ms-python.python}{Python}et
\href{https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens}{GitLens} \href{https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens}{GitLens}
\item \item
\href{https://www.jetbrains.com/pycharm/}{PyCharm} \href{https://www.jetbrains.com/pycharm/}{PyCharm}
\item \item
\href{https://www.vim.org/}{Vim} avec les plugins \href{https://www.vim.org/}{Vim} avec les plugins
\href{https://github.com/davidhalter/jedi-vim}{Jedi-Vim} et \href{https://github.com/davidhalter/jedi-vim}{Jedi-Vim} et
\href{https://github.com/preservim/nerdtree}{nerdtree} \href{https://github.com/preservim/nerdtree}{nerdtree}
\end{itemize} \end{itemize}
Si vous hésitez, et même si Codium n'est pas le plus léger (la faute à Si vous hésitez, et même si Codium n'est pas le plus léger (la faute à \href{https://www.electronjs.org/}{Electron}\ldots\hspace{0pt}), il fera correctement son travail (à savoir : faciliter le vôtre), en intégrant suffisament de fonctionnalités qui gâteront les papilles émoustillées du développeur impatient.
\href{https://www.electronjs.org/}{Electron}\ldots\hspace{0pt}), il fera
correctement son travail (à savoir: faciliter le vôtre), en intégrant
suffisament de fonctionnalités qui gâteront les papilles émoustillées du
développeur impatient.
\begin{figure}[H] \begin{figure}[H]
\centering \centering
@ -39,23 +35,20 @@ développeur impatient.
\subsection{Mode debug - launch.json} \subsection{Mode debug - launch.json}
\section{Terminal} \section{Terminal}
\emph{A priori}, les IDE proposés ci-dessus fournissent par défaut ou \emph{via} des greffons un terminal intégré. \emph{A priori}, les IDE proposés ci-dessus fournissent par défaut ou \emph{via} des greffons un terminal intégré.
Ceci dit, disposer d'un terminal séparé facilite parfois certaines tâches. Ceci dit, disposer d'un terminal séparé facilite parfois certaines tâches.
A nouveau, si vous manquez d'idées: A nouveau, si vous manquez d'idées :
\begin{enumerate} \begin{enumerate}
\item Soit vous utilisez celui qui intégré à VSCodium et qui fera suffisament bien son travail \item
\item Soit vous utilisez celui qui intégré à VSCodium et qui fera suffisament bien son travail
Si vous êtes sous Windows, téléchargez une copie de \item
\href{https://cmder.net/}{Cmder}. Il n'est pas le plus rapide, mais Si vous êtes sous Windows, téléchargez une copie de \href{https://cmder.net/}{Cmder}.
propose une intégration des outils Unix communs (\texttt{ls}, Il n'est pas le plus rapide, mais propose une intégration des outils Unix communs (\texttt{ls}, \texttt{pwd}, \texttt{grep}, \texttt{ssh}, \texttt{git}, \ldots\hspace{0pt}) sans trop se fouler.
\texttt{pwd}, \texttt{grep}, \texttt{ssh}, \texttt{git}, \item
\ldots\hspace{0pt}) sans trop se fouler. Pour tout autre système, vous devriez disposer en natif de ce qu'il faut.
\item
Pour tout autre système, vous devriez disposer en natif de ce qu'il faut.
\end{enumerate} \end{enumerate}
\begin{figure}[H] \begin{figure}[H]
@ -67,16 +60,16 @@ A nouveau, si vous manquez d'idées:
\section{Un gestionnaire de mots de passe} \section{Un gestionnaire de mots de passe}
Nous en auront besoin pour gé(né)rer des phrases secrètes pour nos applications. Nous en auront besoin pour gé(né)rer des phrases secrètes pour nos applications.
Si vous n'en utilisez pas déjà un, partez sur \href{https://keepassxc.org/}{KeepassXC}: il est multi-plateformes, suivi et s'intègre correctement aux différents environnements, tout en restant accessible. Si vous n'en utilisez pas déjà un, partez sur \href{https://keepassxc.org/}{KeepassXC}: il est multi-plateformes, suivi et s'intègre correctement aux différents environnements, tout en restant accessible.
\includegraphics{images/environment/keepass.png} \includegraphics{images/environment/keepass.png}
\subsection{Un système de gestion de versions} \subsection{Un système de gestion de versions}
Il existe plusieurs systèmes de gestion de versions. Il existe plusieurs systèmes de gestion de versions.
Le plus connu/utilisé à l'heure actuelle est \href{https://git-scm.com/}{Git}, notamment pour sa (très) grande flexibilité et sa rapidité d'exécution. Le plus connu/utilisé à l'heure actuelle est \href{https://git-scm.com/}{Git}, notamment pour sa (très) grande flexibilité et sa rapidité d'exécution.
Il est une aide précieuse pour développer rapidement des preuves de concept, switcher vers une nouvelle fonctionnalité, un bogue à réparer ou une nouvelle release à proposer au téléchargement. Il est une aide précieuse pour développer rapidement des preuves de concept, switcher vers une nouvelle fonctionnalité, un bogue à réparer ou une nouvelle release à proposer au téléchargement.
Ses deux plus gros défauts concernent sa courbe d'apprentissage pour les nouveaux venus et la complexité des actions qu'il permet de réaliser. Ses deux plus gros défauts concernent sa courbe d'apprentissage pour les nouveaux venus et la complexité des actions qu'il permet de réaliser.
\begin{figure}[H] \begin{figure}[H]
@ -89,7 +82,7 @@ Même pour un développeur solitaire, un système de gestion de versions (quel q
Chaque "\textbf{branche}" correspond à une tâche à réaliser: un bogue à corriger (\emph{Hotfix A}), une nouvelle fonctionnalité à ajouter ou un "\emph{truc à essayer}" \footnote{Oui, comme dans "Attends, j'essaie vite un truc, si ça marche, c'est beau."} (\emph{Feature A} et \emph{Feature B}). Chaque "\textbf{branche}" correspond à une tâche à réaliser: un bogue à corriger (\emph{Hotfix A}), une nouvelle fonctionnalité à ajouter ou un "\emph{truc à essayer}" \footnote{Oui, comme dans "Attends, j'essaie vite un truc, si ça marche, c'est beau."} (\emph{Feature A} et \emph{Feature B}).
Chaque "\textbf{commit}" correspond à une sauvegarde atomique d'un état ou d'un ensemble de modifications cohérentes entre elles.\footnote{Il convient donc de s'abstenir de modifier le CSS d'une application et la couche d'accès à la base de données, sous peine de se faire huer par ses relecteurs au prochain stand-up.} Chaque "\textbf{commit}" correspond à une sauvegarde atomique d'un état ou d'un ensemble de modifications cohérentes entre elles.\footnote{Il convient donc de s'abstenir de modifier le CSS d'une application et la couche d'accès à la base de données, sous peine de se faire huer par ses relecteurs au prochain stand-up.}
De cette manière, il est beaucoup plus facile pour le développeur de se concenter sur un sujet en particulier, dans la mesure où celui-ci ne doit pas obligatoirement être clôturé pour appliquer un changement de contexte. De cette manière, il est beaucoup plus facile pour le développeur de se concenter sur un sujet en particulier, dans la mesure où celui-ci ne doit pas obligatoirement être clôturé pour appliquer un changement de contexte.
\begin{figure}[H] \begin{figure}[H]
@ -98,45 +91,42 @@ De cette manière, il est beaucoup plus facile pour le développeur de se concen
\caption{Git en action} \caption{Git en action}
\end{figure} \end{figure}
Cas pratique: vous développez cette nouvelle fonctionnalité qui va révolutionner le monde de demain et d'après-demain, quand, tout à coup (!), vous vous rendez compte que vous avez perdu votre conformité aux Cas pratique: vous développez cette nouvelle fonctionnalité qui va révolutionner le monde de demain et d'après-demain, quand, tout à coup (!), vous vous rendez compte que vous avez perdu votre conformité aux normes PCI parce les données des titulaires de cartes ne sont pas isolées correctement.
normes PCI parce les données des titulaires de cartes ne sont pas isolées correctement.
Il suffit alors de: Il suffit alors de:
\begin{enumerate} \begin{enumerate}
\item \item
Sauver le travail en cours (\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ {[}WIP{]}}) Sauver le travail en cours (\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ {[}WIP{]}})
\item \item
Revenir sur la branche principale (\texttt{git\ checkout\ main}) Revenir sur la branche principale (\texttt{git\ checkout\ main})
\item \item
Créer un "hotfix" (\texttt{git\ checkout\ -b\ hotfix/pci-compliance}) Créer un "hotfix" (\texttt{git\ checkout\ -b\ hotfix/pci-compliance})
\item \item
Solutionner le problème (sans doute un \texttt{;} en trop ?) Solutionner le problème (sans doute un \texttt{;} en trop ?)
\item \item
Sauver le correctif sur cette branche Sauver le correctif sur cette branche (\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ "Did\ it!"})
(\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ "Did\ it!"}) \item
\item Récupérer ce correctif sur la branche principal (\texttt{git\ checkout\ main\ \&\&\ git\ merge\ hotfix/pci-compliance})
Récupérer ce correctif sur la branche principal \item
(\texttt{git\ checkout\ main\ \&\&\ git\ merge\ hotfix/pci-compliance}) Et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que l'univers vous réclame à cor et à a cri (\texttt{git\ checkout\ features/dinolol})
\item
Et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que l'univers vous réclame à cor et à a cri (\texttt{git\ checkout\ features/dinolol})
\end{enumerate} \end{enumerate}
Finalement, sachez qu'il existe plusieurs manières de gérer ces flux d'informations. Finalement, sachez qu'il existe plusieurs manières de gérer ces flux d'informations.
Les plus connus sont \href{https://www.gitflow.com/}{Gitflow} et \href{https://www.reddit.com/r/programming/comments/7mfxo6/a_branching_strategy_simpler_than_gitflow/}{Threeflow}. Les plus connus sont \href{https://www.gitflow.com/}{Gitflow} et \href{https://www.reddit.com/r/programming/comments/7mfxo6/a_branching_strategy_simpler_than_gitflow/}{Threeflow}.
\subsection{Décrire ses changements} \subsection{Décrire ses changements}
La description d'un changement se fait \emph{via} la commande \texttt{git\ commit}. La description d'un changement se fait \emph{via} la commande \texttt{git\ commit}.
Il est possible de lui passer directement le message associé à ce changement grâce à l'attribut \texttt{-m}, mais c'est une pratique relativement déconseillée: un \emph{commit} ne doit effectivement pas obligatoirement être décrit sur une seule ligne. Il est possible de lui passer directement le message associé à ce changement grâce à l'attribut \texttt{-m}, mais c'est une pratique relativement déconseillée: un \emph{commit} ne doit effectivement pas obligatoirement être décrit sur une seule ligne.
Une description plus complète, accompagnée des éventuels tickets ou références, sera plus complète, plus agréable à lire, et plus facile à revoir pour vos éventuels relecteurs. Une description plus complète, accompagnée des éventuels tickets ou références, sera plus complète, plus agréable à lire, et plus facile à revoir pour vos éventuels relecteurs.
De plus, la plupart des plateformes de dépôts présenteront ces informations de manière ergonomique. Par exemple: De plus, la plupart des plateformes de dépôts présenteront ces informations de manière ergonomique. Par exemple:
\begin{figure}[H] \begin{figure}[H]
\centering \centering
\includegraphics{images/environment/gitea-commit-message.png} \includegraphics{images/environment/gitea-commit-message.png}
\caption{Un exemple de commit affiché dans Gitea} \caption{Un exemple de commit affiché dans Gitea}
\end{figure} \end{figure}
La première ligne est reprise comme titre (normalement, sur 50 caractères maximum); le reste est repris comme de la description. La première ligne est reprise comme titre (normalement, sur 50 caractères maximum); le reste est repris comme de la description.
@ -144,11 +134,11 @@ La première ligne est reprise comme titre (normalement, sur 50 caractères maxi
\section{Bases de données} \section{Bases de données}
\begin{quote} \begin{quote}
The database is really nothing more than a big bucket of bits where we store our data on a long term basis The database is really nothing more than a big bucket of bits where we store our data on a long term basis
\cite[p. 281]{clean_architecture} \cite[p. 281]{clean_architecture}
\end{quote} \end{quote}
Django gère plusieurs moteurs de base de données. Django gère plusieurs moteurs de base de données.
Certains sont gérés nativement (PostgreSQL, MariaDB, SQLite); et \emph{a priori}, ces trois-là sont disponibles pour tous les systèmes d'exploitation. Certains sont gérés nativement (PostgreSQL, MariaDB, SQLite); et \emph{a priori}, ces trois-là sont disponibles pour tous les systèmes d'exploitation.
D'autres moteurs nécessitent des librairies tierces (Oracle, Microsoft SQL Server). D'autres moteurs nécessitent des librairies tierces (Oracle, Microsoft SQL Server).
@ -157,19 +147,19 @@ D'autres moteurs nécessitent des librairies tierces (Oracle, Microsoft SQL Serv
Parfois, SQLite peut être une bonne option: Parfois, SQLite peut être une bonne option:
\begin{quote} \begin{quote}
Write througput is the area where SQLite struggles the most, but there's not a ton of compelling data online about how it fares, so I got some of my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly modified1 version of the SQLite kvtest2 program on it. Write througput is the area where SQLite struggles the most, but there's not a ton of compelling data online about how it fares, so I got some of my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly modified1 version of the SQLite kvtest2 program on it.
Writing 512 byte blobs as separate transactions, in WAL mode with synchronous=normal3, temp\_store=memory, and mmap enabled, I got 13.78$\mu$s per write, or \textasciitilde72,568 writes per second. Going a bit larger, at 32kb writes, I got 303.74$\mu$s per write, or \textasciitilde3,292 writes per second. Writing 512 byte blobs as separate transactions, in WAL mode with synchronous=normal3, temp\_store=memory, and mmap enabled, I got 13.78$\mu$s per write, or \textasciitilde72,568 writes per second. Going a bit larger, at 32kb writes, I got 303.74$\mu$s per write, or \textasciitilde3,292 writes per second.
That's not astronomical, but it's certainly way more than most websites being used by humans need. That's not astronomical, but it's certainly way more than most websites being used by humans need.
If you had 10 million daily active users, each one could get more than 600 writes per day with that. If you had 10 million daily active users, each one could get more than 600 writes per day with that.
\end{quote} \end{quote}
\begin{quote} \begin{quote}
Looking at read throughput, SQLite can go pretty far: with the same test above, I got a read throughput of \textasciitilde496,770 reads/sec (2.013$\mu$s/read) for the 512 byte blob. Looking at read throughput, SQLite can go pretty far: with the same test above, I got a read throughput of \textasciitilde496,770 reads/sec (2.013$\mu$s/read) for the 512 byte blob.
Other people also report similar results Other people also report similar results
--- Expensify reports that you can get 4M QPS if you're willing to make some slightly more involved changes and use a beefier server. --- Expensify reports that you can get 4M QPS if you're willing to make some slightly more involved changes and use a beefier server.
Four million QPS is enough that every internet user in the world could make \textasciitilde70 queries per day, with a little headroom left over. Four million QPS is enough that every internet user in the world could make \textasciitilde70 queries per day, with a little headroom left over.
Most websites don't need that kind of throughput. \cite{consider_sqlite} Most websites don't need that kind of throughput. \cite{consider_sqlite}
\end{quote} \end{quote}
\subsection{MySQL/MariaDB} \subsection{MySQL/MariaDB}
@ -178,23 +168,16 @@ Most websites don't need that kind of throughput. \cite{consider_sqlite}
\subsection{Gestionnaires} \subsection{Gestionnaires}
Il n'est pas obligatoire de disposer d'une application de gestion pour ces moteurs: pour les cas d'utilisation simples, le shell Django pourra largement suffire (nous y reviendrons). Il n'est pas obligatoire de disposer d'une application de gestion pour ces moteurs: pour les cas d'utilisation simples, le shell Django pourra largement suffire (nous y reviendrons).
Mais pour faciliter la gestion des bases de données elles-même, et si vous n'êtes pas à l'aise avec la Mais pour faciliter la gestion des bases de données elles-même, et si vous n'êtes pas à l'aise avec la ligne de commande, choisissez l'une des applications d'administration ci-dessous en fonction du moteur de base de données que vous souhaitez utiliser.
ligne de commande, choisissez l'une des applications d'administration
ci-dessous en fonction du moteur de base de données que vous souhaitez
utiliser.
\begin{itemize} \begin{itemize}
\item \item
Pour \textbf{PostgreSQL}, il existe Pour \textbf{PostgreSQL}, il existe \href{https://www.pgadmin.org/}{pgAdmin}
\href{https://www.pgadmin.org/}{pgAdmin} \item
\item Pour \textbf{MariaDB} ou \textbf{MySQL}, partez sur \href{https://www.phpmyadmin.net/}{PHPMyAdmin}
Pour \textbf{MariaDB} ou \textbf{MySQL}, partez sur \item
\href{https://www.phpmyadmin.net/}{PHPMyAdmin} Pour \textbf{SQLite}, il existe \href{https://sqlitebrowser.org/}{SQLiteBrowser} PHPMyAdmin ou PgAdmin.
\item
Pour \textbf{SQLite}, il existe
\href{https://sqlitebrowser.org/}{SQLiteBrowser} PHPMyAdmin ou
PgAdmin.
\end{itemize} \end{itemize}
\section{Intégration continue} \section{Intégration continue}
@ -202,4 +185,3 @@ utiliser.
\subsection{Qualité du code} \subsection{Qualité du code}
Code climate, SonarQube. Code climate, SonarQube.

View File

@ -1,10 +1,9 @@
\chapter{URLs et espaces de noms} \chapter{URLs et espaces de noms}
La gestion des URLs permet \textbf{grosso modo} d'assigner une adresse La gestion des URLs permet \textbf{grosso modo} d'assigner une adresse paramétrée ou non à une fonction Python.
paramétrée ou non à une fonction Python. La manière simple consiste à La manière simple consiste à modifier le fichier \texttt{gwift/settings.py} pour y ajouter nos correspondances.
modifier le fichier \texttt{gwift/settings.py} pour y ajouter nos Par défaut, le fichier ressemble à ceci:
correspondances. Par défaut, le fichier ressemble à ceci:
\begin{minted}{python} \begin{minted}{python}
# gwift/urls.py # gwift/urls.py
@ -18,24 +17,12 @@ urlpatterns = [
\end{minted} \end{minted}
La variable \texttt{urlpatterns} associe un ensemble d'adresses à des La variable \texttt{urlpatterns} associe un ensemble d'adresses à des fonctions.
fonctions. Dans le fichier \textbf{nu}, seul le \textbf{pattern} Dans le fichier \textbf{nu}, seul le \textbf{pattern} \texttt{admin} est défini, et inclut toutes les adresses qui sont définies dans le fichier \texttt{admin.site.urls}.
\texttt{admin} est défini, et inclut toutes les adresses qui sont
définies dans le fichier \texttt{admin.site.urls}.
Django fonctionne avec des \textbf{expressions rationnelles} simplifiées
(des \textbf{expressions régulières} ou \textbf{regex}) pour trouver une
correspondance entre une URL et la fonction qui recevra la requête et
retournera une réponse. Nous utilisons l'expression \texttt{\^{}\$} pour
déterminer la racine de notre application, mais nous pourrions appliquer
d'autres regroupements (\texttt{/home},
\texttt{users/\textless{}profile\_id\textgreater{}},
\texttt{articles/\textless{}year\textgreater{}/\textless{}month\textgreater{}/\textless{}day\textgreater{}},
\ldots\hspace{0pt}). Chaque \textbf{variable} déclarée dans l'expression
régulière sera apparenté à un paramètre dans la fonction correspondante.
Ainsi, pour reprendre l'exemple où nous étions restés:
Django fonctionne avec des \textbf{expressions rationnelles} simplifiées (des \textbf{expressions régulières} ou \textbf{regex}) pour trouver une correspondance entre une URL et la fonction qui recevra la requête et retournera une réponse.
Nous utilisons l'expression \texttt{\^{}\$} pour déterminer la racine de notre application, mais nous pourrions appliquer d'autres regroupements (\texttt{/home}, \texttt{users/\textless{}profile\_id\textgreater{}}, \texttt{articles/\textless{}year\textgreater{}/\textless{}month\textgreater{}/\textless{}day\textgreater{}}, \ldots\hspace{0pt}).
Chaque \textbf{variable} déclarée dans l'expression régulière sera apparenté à un paramètre dans la fonction correspondante. Ainsi, pour reprendre l'exemple où nous étions restés:
\begin{minted}{python} \begin{minted}{python}
# gwift/urls.py # gwift/urls.py
@ -51,18 +38,12 @@ urlpatterns = [
] ]
\end{minted} \end{minted}
Dans la mesure du possible, essayez toujours de \textbf{nommer} chaque expression.
Cela permettra notamment de les retrouver au travers de la fonction \texttt{reverse}, mais permettra également de simplifier vos templates.
Dans la mesure du possible, essayez toujours de \textbf{nommer} chaque A présent, on doit tester que l'URL racine de notre application mène bien vers la fonction \texttt{wish\_views.wishlists}.
expression. Cela permettra notamment de les retrouver au travers de la
fonction \texttt{reverse}, mais permettra également de simplifier vos
templates.
A présent, on doit tester que l'URL racine de notre application mène Sauf que les pages \texttt{about} et \texttt{help} existent également. Pour implémenter ce type de précédence, il faudrait implémenter les URLs de la manière suivante:
bien vers la fonction \texttt{wish\_views.wishlists}.
Sauf que les pages \texttt{about} et \texttt{help} existent également.
Pour implémenter ce type de précédence, il faudrait implémenter les URLs
de la manière suivante:
\begin{verbatim} \begin{verbatim}
| about | about
@ -70,32 +51,21 @@ de la manière suivante:
| <user> | <user>
\end{verbatim} \end{verbatim}
Mais cela signifie aussi que les utilisateurs \texttt{about} et Mais cela signifie aussi que les utilisateurs \texttt{about} et \texttt{help} (s'ils existent\ldots\hspace{0pt}) ne pourront jamais accéder à leur profil.
\texttt{help} (s'ils existent\ldots\hspace{0pt}) ne pourront jamais Une dernière solution serait de maintenir une liste d'authorité des noms d'utilisateur qu'il n'est pas possible d'utiliser.
accéder à leur profil. Une dernière solution serait de maintenir une
liste d'authorité des noms d'utilisateur qu'il n'est pas possible
d'utiliser.
D'où l'importance de bien définir la séquence de déinition de ces D'où l'importance de bien définir la séquence de déinition de ces routes, ainsi que des espaces de noms.
routes, ainsi que des espaces de noms.
Note sur les namespaces. Note sur les namespaces.
De là, découle une autre bonne pratique: l'utilisation de De là, découle une autre bonne pratique: l'utilisation de \emph{breadcrumbs} (\url{https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template}) ou de guidelines de navigation.
\emph{breadcrumbs}
(\url{https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template})
ou de guidelines de navigation.
\section{Reverse} \section{Reverse}
En associant un nom ou un libellé à chaque URL, il est possible de récupérer sa \textbf{traduction}. Cela implique par contre de ne plus toucher à ce libellé par la suite\ldots\hspace{0pt}
En associant un nom ou un libellé à chaque URL, il est possible de Dans le fichier \texttt{urls.py}, on associe le libellé \texttt{wishlists} à l'URL \texttt{r\textquotesingle{}\^{}\$} (c'est-à-dire la racine du site):
récupérer sa \textbf{traduction}. Cela implique par contre de ne plus
toucher à ce libellé par la suite\ldots\hspace{0pt}
Dans le fichier \texttt{urls.py}, on associe le libellé
\texttt{wishlists} à l'URL \texttt{r\textquotesingle{}\^{}\$}
(c'est-à-dire la racine du site):
\begin{minted}{python} \begin{minted}{python}
from wish.views import WishListList from wish.views import WishListList
@ -106,17 +76,13 @@ urlpatterns = [
] ]
\end{minted} \end{minted}
De cette manière, dans nos templates, on peut à présent construire un lien vers la racine avec le tags suivant:
De cette manière, dans nos templates, on peut à présent construire un
lien vers la racine avec le tags suivant:
\begin{minted}{html} \begin{minted}{html}
<a href="{% url 'wishlists' %}">{{ yearvar }} Archive</a> <a href="{% url 'wishlists' %}">{{ yearvar }} Archive</a>
\end{minted} \end{minted}
De la même manière, on peut également récupérer l'URL de destination pour n'importe quel libellé, de la manière suivante:
De la même manière, on peut également récupérer l'URL de destination
pour n'importe quel libellé, de la manière suivante:
\begin{minted}{python} \begin{minted}{python}
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy

View File

@ -1,29 +1,23 @@
\chapter{Travailler en isolation} \chapter{Travailler en isolation}
Nous allons aborder la gestion et l'isolation des dépendances. Cette Nous allons aborder la gestion et l'isolation des dépendances. Cette section est aussi utile pour une personne travaillant seule, que pour transmettre les connaissances à un nouveau membre de l'équipe ou pour déployer l'application elle-même.
section est aussi utile pour une personne travaillant seule, que pour
transmettre les connaissances à un nouveau membre de l'équipe ou pour
déployer l'application elle-même.
Il en était déjà question au deuxième point des 12 facteurs: même dans Il en était déjà question au deuxième point des 12 facteurs: même dans le cas de petits projets, il est déconseillé de s'en passer.
le cas de petits projets, il est déconseillé de s'en passer. Cela évite Cela évite les déploiements effectués à l'arrache à grand renfort de \texttt{sudo} et d'installation globale de dépendances, pouvant potentiellement occasioner des conflits entre les applications déployées:
les déploiements effectués à l'arrache à grand renfort de \texttt{sudo}
et d'installation globale de dépendances, pouvant potentiellement
occasioner des conflits entre les applications déployées:
\begin{enumerate} \begin{enumerate}
\item \item
Il est tout à fait envisagable que deux applications différentes soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance. Il est tout à fait envisagable que deux applications différentes soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance.
\item \item
Pour la reproductibilité d'un environnement spécifique, cela évite notamment les réponses type "Ca marche chez moi", puisque la construction du nouvel environnement fait partie intégrante du processus de développement et de la documentation du projet; grâce à elle, nous avons la possibilité de construire un environnement sain et d'appliquer des dépendances identiques, quelle que soit l'hôte destiné à accueillir le déploiment. Pour la reproductibilité d'un environnement spécifique, cela évite notamment les réponses type "Ca marche chez moi", puisque la construction du nouvel environnement fait partie intégrante du processus de développement et de la documentation du projet; grâce à elle, nous avons la possibilité de construire un environnement sain et d'appliquer des dépendances identiques, quelle que soit l'hôte destiné à accueillir le déploiment.
\end{enumerate} \end{enumerate}
\begin{figure} \begin{figure}
\centering \centering
\includegraphics{images/it-works-on-my-machine.jpg} \includegraphics{images/it-works-on-my-machine.jpg}
\caption{It works on my machine} \caption{It works on my machine}
\end{figure} \end{figure}
\section{Environnements virtuels} \section{Environnements virtuels}
@ -32,29 +26,28 @@ occasioner des conflits entre les applications déployées:
\includegraphics{images/xkcd-1987.png} \includegraphics{images/xkcd-1987.png}
\caption{\url{https://xkcd.com/1987}} \caption{\url{https://xkcd.com/1987}}
\end{figure} \end{figure}
Un des reproches que l'on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont Un des reproches que l'on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont pas toujours simples ou directes.
pas toujours simples ou directes. Pour quelqu'un qui débarquererait, la quantité d'options différentes peut paraître rebutante.
Pour quelqu'un qui débarquererait, la quantité d'options différentes peut paraître rebutante. Nous pensons notamment aux environnements virtuels: ils sont géniaux à utiliser, mais on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version améliorée et plus ergonomique), \texttt{venv} (la version intégrée depuis la version 3.3 de l'interpréteur, et \href{https://docs.python.org/3/library/venv.html}{la manière recommandée} de créer un environnement depuis la 3.5). Nous pensons notamment aux environnements virtuels : ils sont géniaux à utiliser, mais on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version améliorée et plus ergonomique), \texttt{venv} (la version intégrée depuis la version 3.3 de l'interpréteur, et \href{https://docs.python.org/3/library/venv.html}{la manière recommandée} de créer un environnement depuis la 3.5).
Pour créer un nouvel environnement, vous aurez donc besoin: Pour créer un nouvel environnement, vous aurez donc besoin:
\begin{enumerate} \begin{enumerate}
\item \item
D'une installation de Python - \url{https://www.python.org/} D'une installation de Python - \url{https://www.python.org/}
\item \item
D'un terminal - voir le point \href{../environment/_index.xml\#un-terminal}{Un terminal} D'un terminal - voir le point \href{../environment/_index.xml\#un-terminal}{Un terminal}
\end{enumerate} \end{enumerate}
Il existe plusieurs autres modules permettant d'arriver au même résultat, avec quelques avantages et inconvénients pour chacun d'entre eux. Il existe plusieurs autres modules permettant d'arriver au même résultat, avec quelques avantages et inconvénients pour chacun d'entre eux.
Le plus prometteur d'entre eux est \href{https://python-poetry.org/}{Poetry}, qui dispose d'une interface Le plus prometteur d'entre eux est \href{https://python-poetry.org/}{Poetry}, qui dispose d'une interface en ligne de commande plus propre et plus moderne que ce que PIP propose.
en ligne de commande plus propre et plus moderne que ce que PIP propose.
\subsection{Poetry} \subsection{Poetry}
Poetry se propose de gérer le projet au travers d'un fichier pyproject.toml. Poetry se propose de gérer le projet au travers d'un fichier pyproject.toml.
TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à ses heures), se place comme alternative aux formats comme JSON, YAML ou INI. TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à ses heures), se place comme alternative aux formats comme JSON, YAML ou INI.
@ -63,12 +56,12 @@ TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à
$ tree django-gecko/ $ tree django-gecko/
django-gecko/ django-gecko/
django_gecko django_gecko
__init__.py __init__.py
pyproject.toml pyproject.toml
README.rst README.rst
tests tests
__init__.py __init__.py
test_django_gecko.py test_django_gecko.py
2 directories, 5 files 2 directories, 5 files
\end{verbatim} \end{verbatim}
@ -76,15 +69,14 @@ TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à
Ceci signifie que nous avons directement (et de manière standard): Ceci signifie que nous avons directement (et de manière standard):
\begin{itemize} \begin{itemize}
\item \item
Un répertoire django-gecko, qui porte le nom de l'application que vous Un répertoire django-gecko, qui porte le nom de l'application que vous venez de créer
venez de créer \item
\item Un répertoires tests, libellé selon les standards de pytest
Un répertoires tests, libellé selon les standards de pytest \item
\item Un fichier README.rst (qui ne contient encore rien)
Un fichier README.rst (qui ne contient encore rien) \item
\item Un fichier pyproject.toml, qui contient ceci:
Un fichier pyproject.toml, qui contient ceci:
\end{itemize} \end{itemize}
\begin{verbatim} \begin{verbatim}
@ -105,37 +97,26 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
\end{verbatim} \end{verbatim}
La commande \texttt{poetry\ init} permet de générer interactivement les La commande \texttt{poetry\ init} permet de générer interactivement les fichiers nécessaires à son intégration dans un projet existant.
fichiers nécessaires à son intégration dans un projet existant.
J'ai pour habitude de conserver mes projets dans un répertoire 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/}.
\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 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.
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. 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. 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 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}.
contenant cet environnement, il est primordial de \textbf{ne pas le 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.
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, Pur créer notre répertoire de travail et notre environnement virtuel, exécutez les commandes suivantes:
exécutez les commandes suivantes:
\begin{verbatim} \begin{verbatim}
mkdir ~/.venvs/ mkdir ~/.venvs/
python -m venv ~/.venvs/gwift-venv python -m venv ~/.venvs/gwift-venv
\end{verbatim} \end{verbatim}
Ceci aura pour effet de créer un nouveau répertoire 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.
(\texttt{\textasciitilde{}/.venvs/gwift-env/}), dans lequel vous 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:
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} \begin{verbatim}
# GNU/Linux, macOS # GNU/Linux, macOS
@ -145,48 +126,33 @@ souhaitons l'utiliser, grâce à l'une des commandes suivantes:
# Pour les deux # Pour les deux
(gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$ (gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$
\end{verbatim} \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 A présent que l'environnement est activé, tous les binaires de cet environnement prendront le pas sur les binaires du système.
manière, une variable \texttt{PATH} propre est définie et utilisée, afin 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.
que les librairies Python y soient stockées. C'est donc dans cet 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
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. aurons installées.
Pour les curieux, un environnement virtuel n'est jamais qu'un répertoire 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.
dans lequel se trouve une installation fraîche de l'interpréteur, vers 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}.
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 Pour sortir de l'environnement virtuel, exécutez la commande \texttt{deactivate}.
\texttt{deactivate}. Si vous pensez ne plus en avoir besoin, supprimer Si vous pensez ne plus en avoir besoin, supprimer le dossier.
le dossier. Si nécessaire, il suffira d'en créer un nouveau. 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 Pour gérer des versions différentes d'une même librairie, il nous suffit de jongler avec autant d'environnements que nécessaires.
de jongler avec autant d'environnements que nécessaires. Une application 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.
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 Cette technique fonctionnera autant pour un poste de développement que sur les serveurs destinés à recevoir notre application.
sur les serveurs destinés à recevoir notre application.
Par la suite, nous considérerons que l'environnement virtuel est Par la suite, nous considérerons que l'environnement virtuel est toujours activé, même si \texttt{gwift-env} n'est pas indiqué.
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 a manière recommandée pour la gestion des dépendances consiste à les épingler dans un fichier requirements.txt, placé à la racine du projet.
é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.
Ce fichier reprend, ligne par ligne, chaque dépendance et la version Cet épinglage est cependant relativement basique, dans la mesure où les opérateurs disponibles sont ==, et \textgreater=.
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 Poetry propose un épinglage basé sur SemVer.
être appliquées aux dépendances sont plus touffues que ce que proposent 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:
pip -r, avec la présence du curseur \^{}, qui ne modifiera pas le nombre
différent de zéro le plus à gauche:
\begin{verbatim} \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 ^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
@ -197,7 +163,7 @@ différent de zéro le plus à gauche:
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). 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. 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) 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). et de qualité (tous les environnements utilisent la même version d'une dépendance).
@ -238,27 +204,27 @@ Django = "^3.2.3"
[...] [...]
\end{verbatim} \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}). 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 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}. requirements, puisqu'elles s'y ajoutent automatiquement grâce à la commande \texttt{add}.
\subsubsection{Python Packaging Made Easy} \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). 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é. 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. 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: 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} \begin{quote}
Python packaging can be a bit overwhelming at first. The main reason for Python packaging can be a bit overwhelming at first. The main reason for
that is the confusion about proper tools for creating Python packages. 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 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 hard as it looks. Also, knowing propre, state-of-the-art packaging helps
a lot. a lot.
\end{quote} \end{quote}
En gros, c'est ardu-au-début-mais-plus-trop-après. 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}}). Et c'est heureusement suivi et documenté par la PyPA (\textbf{\href{https://github.com/pypa}{Python Packaging Authority}}).
Les étapes sont les suivantes: Les étapes sont les suivantes:
@ -274,7 +240,7 @@ Les étapes sont les suivantes:
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}. 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} \end{enumerate}
Avec Poetry, deux commandes suffisent (théoriquement - puisque je n'ai pas essayé): \texttt{poetry\ build} et \texttt{poetry\ publish}: Avec Poetry, deux commandes suffisent (théoriquement - puisque je n'ai pas essayé) : \texttt{poetry\ build} et \texttt{poetry\ publish}:
\begin{verbatim} \begin{verbatim}
$ poetry build $ poetry build
@ -297,8 +263,7 @@ Ce qui est quand même 'achement plus simple que d'appréhender tout un
\section{Un système de virtualisation} \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 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 développement aussi proche que celui en production.
développement aussi proche que celui en production.
Les solutions sont nombreuses: Les solutions sont nombreuses:
\begin{itemize} \begin{itemize}
@ -314,7 +279,7 @@ Les solutions sont nombreuses:
\href{https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v}{Hyper-V} \href{https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v}{Hyper-V}
\end{itemize} \end{itemize}
Ces quelques propositions se situent un cran plus loin que la "simple" isolation d'un environnement, puisqu'elles vous permettront de construire un environnement complet. Ces quelques propositions se situent un cran plus loin que la "simple" isolation d'un environnement, puisqu'elles vous permettront de construire un environnement complet.
Elles constituent donc une étape supplémentaires dans la configuration de votre espace de travail, mais en amélioreront la qualité. Elles constituent donc une étape supplémentaires dans la configuration de votre espace de travail, mais en amélioreront la qualité.
Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux solutions automatisables et multiplateformes, dont la configuration peut faire partie intégrante de vos sources. Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux solutions automatisables et multiplateformes, dont la configuration peut faire partie intégrante de vos sources.
@ -329,50 +294,37 @@ Vagrant consiste en un outil de création et de gestion d'environnements virtual
automation, Vagrant lowers development environment setup time, increases automation, Vagrant lowers development environment setup time, increases
production parity, and makes the "works on my machine" excuse a relic of production parity, and makes the "works on my machine" excuse a relic of
the past. \footnote{\url{https://www.vagrantup.com/intro}} the past. \footnote{\url{https://www.vagrantup.com/intro}}
\end{quote} \end{quote}
La partie la plus importante de la configuration de Vagrant pour votre La partie la plus importante de la configuration de Vagrant pour votre projet consiste à placer un fichier \texttt{Vagrantfile} - \emph{a priori} à la racine de votre projet - et qui contiendra les information suivantes:
projet consiste à placer un fichier \texttt{Vagrantfile} - \emph{a
priori} à la racine de votre projet - et qui contiendra les information \begin{itemize}
suivantes:
\begin{itemize}
\item \item
Le choix du \emph{fournisseur} (\textbf{provider}) de virtualisation Le choix du \emph{fournisseur} (\textbf{provider}) de virtualisation (Virtualbox, Hyper-V et Docker sont natifs; il est également possible de passer par VMWare, AWS, etc.)
(Virtualbox, Hyper-V et Docker sont natifs; il est également possible
de passer par VMWare, AWS, etc.)
\item \item
Une \emph{box}, qui consiste à lui indiquer le type et la version Une \emph{box}, qui consiste à lui indiquer le type et la version attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et \href{https://app.vagrantup.com/boxes/search}{il y a du choix}).
attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et
\href{https://app.vagrantup.com/boxes/search}{il y a du choix}).
\item \item
La manière dont la fourniture (\textbf{provisioning}) de La manière dont la fourniture (\textbf{provisioning}) de l'environnement doit être réalisée : scripts Shell, fichiers, Ansible, Puppet, Chef, \ldots\hspace{0pt} Choisissez votre favori :-) même s'il est toujours possible de passer par une installation et une maintenance manuelle, après s'être connecté sur la machine.
l'environnement doit être réalisée: scripts Shell, fichiers, Ansible,
Puppet, Chef, \ldots\hspace{0pt} Choisissez votre favori :-) même s'il
est toujours possible de passer par une installation et une
maintenance manuelle, après s'être connecté sur la machine.
\item \item
Si un espace de stockage doit être partagé entre la machine virtuelle Si un espace de stockage doit être partagé entre la machine virtuelle et l'hôte
et l'hôte
\item \item
Les ports qui doivent être transmis de la machine virtuelle vers Les ports qui doivent être transmis de la machine virtuelle vers l'hôte.
l'hôte. \end{itemize}
\end{itemize}
La syntaxe de ce fichier \texttt{Vagrantfile} est en \href{https://www.ruby-lang.org/en/}{Ruby}.
La syntaxe de ce fichier \texttt{Vagrantfile} est en \href{https://www.ruby-lang.org/en/}{Ruby}.
Vous trouverez ci-dessous un exemple, généré (et nettoyé) après avoir exécuté la commande \texttt{vagrant\ init}: Vous trouverez ci-dessous un exemple, généré (et nettoyé) après avoir exécuté la commande \texttt{vagrant\ init}:
\begin{listing}[H] \begin{listing}[H]
\begin{minted}{ruby} \begin{minted}{ruby}
# -*- mode: ruby -*- # -*- mode: ruby -*-
# vi: set ft=ruby : # vi: set ft=ruby :
Vagrant.configure("2") do |config| Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64" config.vm.box = "ubuntu/bionic64"
config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip:
"127.0.0.1" "127.0.0.1"
config.vm.provider "virtualbox" do |vb| config.vm.provider "virtualbox" do |vb|
vb.gui = true vb.gui = true
vb.memory = "1024" vb.memory = "1024"
@ -389,14 +341,14 @@ Vous trouverez ci-dessous un exemple, généré (et nettoyé) après avoir exéc
Dans le fichier ci-dessus, nous créons: Dans le fichier ci-dessus, nous créons:
\begin{itemize} \begin{itemize}
\item \item
Une nouvelle machine virtuelle (ie. \emph{invitée}) sous Ubuntu Bionic Beaver, en x64 Une nouvelle machine virtuelle (ie. \emph{invitée}) sous Ubuntu Bionic Beaver, en x64
\item \item
Avec une correspondance du port \texttt{80} de la machine vers le port \texttt{8080} de l'hôte, en limitant l'accès à celui-ci - accédez à \texttt{localhost:8080} et vous accéderez au port \texttt{80} de la machine virtuelle. Avec une correspondance du port \texttt{80} de la machine vers le port \texttt{8080} de l'hôte, en limitant l'accès à celui-ci - accédez à \texttt{localhost:8080} et vous accéderez au port \texttt{80} de la machine virtuelle.
\item \item
En utilisant Virtualbox comme backend - la mémoire vive allouée sera limitée à 1Go de RAM et nous ne voulons pas voir l'interface graphique au démarrage En utilisant Virtualbox comme backend - la mémoire vive allouée sera limitée à 1Go de RAM et nous ne voulons pas voir l'interface graphique au démarrage
\item \item
Et pour finir, nous voulons appliquer un script de mise à jour \texttt{apt-get\ update} et installer le paquet \texttt{nginx} Et pour finir, nous voulons appliquer un script de mise à jour \texttt{apt-get\ update} et installer le paquet \texttt{nginx}
\end{itemize} \end{itemize}
Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichier \texttt{Vagrantfile} se trouve) sera synchronisé dans le répertoire \texttt{/vagrant} sur la machine invitée. Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichier \texttt{Vagrantfile} se trouve) sera synchronisé dans le répertoire \texttt{/vagrant} sur la machine invitée.
@ -410,7 +362,7 @@ Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichie
\begin{listing}[H] \begin{listing}[H]
\begin{verbatim} \begin{verbatim}
version: '3' version: '3'
volumes: volumes:
local_postgres_data: {} local_postgres_data: {}
local_postgres_data_backups: {} local_postgres_data_backups: {}
@ -442,7 +394,7 @@ Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichie
\end{listing} \end{listing}
\begin{listing}[H] \begin{listing}[H]
\begin{verbatim} \begin{verbatim}
postgres: postgres:
build: build:
context: . context: .
@ -542,7 +494,7 @@ Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichie
- 8000:8000 - 8000:8000
depends_on: depends_on:
- slqserver - slqserver
slqserver: slqserver:
image: mcr.microsoft.com/mssql/server:2019-latest image: mcr.microsoft.com/mssql/server:2019-latest
environment: environment:
@ -562,12 +514,12 @@ Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichie
\begin{listing}[H] \begin{listing}[H]
\begin{verbatim} \begin{verbatim}
FROM python:3.8-slim-buster FROM python:3.8-slim-buster
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
RUN apt-get update \ RUN apt-get update \
# dependencies for building Python packages # dependencies for building Python packages
&& apt-get install -y build-essential \ && apt-get install -y build-essential \
# psycopg2 dependencies # psycopg2 dependencies
@ -578,33 +530,33 @@ Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichie
&& apt-get purge -y --auto-remove -o && apt-get purge -y --auto-remove -o
APT::AutoRemove::RecommendsImportant=false \ APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Requirements are installed here to ensure they will be cached. # Requirements are installed here to ensure they will be cached.
COPY ./requirements /requirements COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt RUN pip install -r /requirements/local.txt
COPY ./compose/production/django/entrypoint /entrypoint COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint RUN chmod +x /entrypoint
COPY ./compose/local/django/start /start COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start RUN sed -i 's/\r$//g' /start
RUN chmod +x /start RUN chmod +x /start
COPY ./compose/local/django/celery/worker/start /start-celeryworker COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r$//g' /start-celeryworker RUN sed -i 's/\r$//g' /start-celeryworker
RUN chmod +x /start-celeryworker RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start /start-celerybeat COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r$//g' /start-celerybeat RUN sed -i 's/\r$//g' /start-celerybeat
RUN chmod +x /start-celerybeat RUN chmod +x /start-celerybeat
COPY ./compose/local/django/celery/flower/start /start-flower COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r$//g' /start-flower RUN sed -i 's/\r$//g' /start-flower
RUN chmod +x /start-flower RUN chmod +x /start-flower
WORKDIR /app WORKDIR /app
ENTRYPOINT ["/entrypoint"] ENTRYPOINT ["/entrypoint"]
\end{verbatim} \end{verbatim}
\end{listing} \end{listing}
@ -617,39 +569,39 @@ Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichie
# Pull base image # Pull base image
FROM python:3.8-slim-buster FROM python:3.8-slim-buster
# Set environment variables # Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
ENV ACCEPT_EULA=Y ENV ACCEPT_EULA=Y
# install Microsoft SQL Server requirements. # install Microsoft SQL Server requirements.
ENV ACCEPT_EULA=Y ENV ACCEPT_EULA=Y
RUN apt-get update -y && apt-get update \ RUN apt-get update -y && apt-get update \
&& apt-get install -y --no-install-recommends curl gcc g++ gnupg && apt-get install -y --no-install-recommends curl gcc g++ gnupg
# Add SQL Server ODBC Driver 17 # Add SQL Server ODBC Driver 17
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y msodbcsql17 unixodbc-dev && apt-get install -y msodbcsql17 unixodbc-dev
# clean the install. # clean the install.
RUN apt-get -y clean RUN apt-get -y clean
# Set work directory # Set work directory
WORKDIR /code WORKDIR /code
# Install dependencies # Install dependencies
COPY ./requirements/base.txt /code/requirements/ COPY ./requirements/base.txt /code/requirements/
RUN pip install --upgrade pip RUN pip install --upgrade pip
RUN pip install -r ./requirements/base.txt RUN pip install -r ./requirements/base.txt
# Copy project # Copy project
COPY . /code/ COPY . /code/
\end{verbatim} \end{verbatim}
\caption{Un exemple de Dockerfile} \caption{Un exemple de Dockerfile}
\end{listing} \end{listing}

View File

@ -19,21 +19,25 @@
--- Robert C. Martin Clean Architecture --- Robert C. Martin Clean Architecture
\end{quote} \end{quote}
Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un Il y a une raison très simple à aborder le déploiement dès maintenant : à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un
serveur. serveur.
En cas de dérogation à ceci, il sera probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles puissent être mises à disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé. Il est du coup probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles puissent être mises à
Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque: l'objectif est qu'il soit rapide et fiable. Ceci peut être atteint au travers d'un partitionnement correct, incluant le fait que le composant principal s'assure que chaque sous-composant est correctement démarré intégré et supervisé. disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé.
Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque.
L'objectif est qu'il soit rapide et fiable.
Ceci peut être atteint au travers d'un partitionnement correct, incluant le fait que le composant principal s'assure que chaque sous-composant est correctement démarré intégré et supervisé.
Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes. A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d'un tronc commun au développement Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes.
A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d'un tronc commun au développement
\cite{devops_handbook}. \cite{devops_handbook}.
Déploier une nouvelle version doit être le plus simple possible, et doit se résumer, dans le pire des cas, à quelques lignes d'un script Bash. Déploier une nouvelle version doit être le plus simple possible, et doit se résumer, dans le pire des cas, à quelques lignes d'un script Bash.
\begin{quote} \begin{quote}
Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and
disruptions such as service outages, service impairments, or security or compliance failures. disruptions such as service outages, service impairments, or security or compliance failures.
--- DevOps Handbook Introduction --- DevOps Handbook Introduction
\end{quote} \end{quote}
Le serveur que Django met à notre disposition \emph{via} la commande \texttt{runserver} est extrêmement pratique, mais il est uniquement prévu pour la phase développement. Le serveur que Django met à notre disposition \emph{via} la commande \texttt{runserver} est extrêmement pratique, mais il est uniquement prévu pour la phase développement.
@ -75,34 +79,22 @@ Ainsi, on trouve:
Dans cette partie, nous aborderons les points suivants: Dans cette partie, nous aborderons les points suivants:
\begin{enumerate} \begin{enumerate}
\item \item
Définir l'infrastructure et les composants nécessaires à notre Définir l'infrastructure et les composants nécessaires à notre application
application \item
\item Configurer l'hôte qui hébergera l'application et y déployer notre application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt. A ce stade, nous aurons déjà une application disponible.
Configurer l'hôte qui hébergera l'application et y déployer notre \item
application: dans une machine physique, virtuelle ou dans un Configurer les outils nécessaires à la bonne exécution de ce code et de ses fonctionnalités: les différentes méthodes de supervision de l'application, comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter correctement l'information.
container. Nous aborderons aussi les déploiements via Ansible et Salt.
A ce stade, nous aurons déjà une application disponible.
\item
Configurer les outils nécessaires à la bonne exécution de ce code et
de ses fonctionnalités: les différentes méthodes de supervision de
l'application, comment analyser les fichiers de logs, comment
intercepter correctement une erreur si elle se présente et comment
remonter correctement l'information.
\end{enumerate} \end{enumerate}
Nous allons détailler ci-dessous trois méthodes de déploiement: Nous allons détailler ci-dessous trois méthodes de déploiement:
\begin{itemize} \begin{itemize}
\item \item
Sur une machine hôte, en embarquant tous les composants sur un même Sur une machine hôte, en embarquant tous les composants sur un même serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de configurer un \emph{load balancer}, de routeur plusieurs basées de données, mais ce sera le premier cas de figure.
serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de
configurer un \emph{load balancer}, de routeur plusieurs basées de \item Dans des containers, avec Docker-Compose.
données, mais ce sera le premier cas de figure.
\item \item
Dans des containers, avec Docker-Compose. Sur une \textbf{Plateforme en tant que Service} (ou plus simplement, \textbf{PaaSPaaS}), pour faire abstraction de toute la couche de configuration du serveur.
\item
Sur une \textbf{Plateforme en tant que Service} (ou plus simplement,
\textbf{PaaSPaaS}), pour faire abstraction de toute la couche de
configuration du serveur.
\end{itemize} \end{itemize}

View File

@ -1,9 +1,9 @@
\part{Environnement et méthodes de travail} \part{Environnement et méthodes de travail}
\begin{quote} \begin{quote}
Make it work, make it right, make it fast Make it work, make it right, make it fast
--- Kent Beck --- Kent Beck
\end{quote} \end{quote}
En fonction de vos connaissances et compétences, la création d'une nouvelle application est une étape relativement facile à mettre en place. En fonction de vos connaissances et compétences, la création d'une nouvelle application est une étape relativement facile à mettre en place.
@ -11,58 +11,40 @@ Le code qui permet de faire tourner cette application peut ne pas être élégan
Les problèmes arriveront lorsqu'une nouvelle demande sera introduite, lorsqu'un bug sera découvert et devra être corrigé ou lorsqu'une dépendance cessera de fonctionner ou d'être disponible. Les problèmes arriveront lorsqu'une nouvelle demande sera introduite, lorsqu'un bug sera découvert et devra être corrigé ou lorsqu'une dépendance cessera de fonctionner ou d'être disponible.
Or, une application qui n'évolue pas, meurt. Or, une application qui n'évolue pas, meurt.
Toute application est donc destinée, soit à être modifiée, corrigée et suivie, soit à déperrir et à être Toute application est donc destinée, soit à être modifiée, corrigée et suivie, soit à déperrir et à être délaissée par ses utilisateurs.
délaissée par ses utilisateurs.
Et c'est juste cette maintenance qui est difficile. Et c'est juste cette maintenance qui est difficile.
L'application des principes présentés et agrégés ci-dessous permet surtout de préparer correctement tout ce qui pourra arriver, sans aller jusqu'au « \textbf{You Ain't Gonna Need It} » (ou \textbf{YAGNI\index{YAGNI}}), qui consiste à surcharger tout développement avec des fonctionnalités non demandées, juste « au cas ou ». L'application des principes présentés et agrégés ci-dessous permet surtout de préparer correctement tout ce qui pourra arriver, sans aller jusqu'au « \textbf{You Ain't Gonna Need It} » (ou \textbf{YAGNI\index{YAGNI}}), qui consiste à surcharger tout développement avec des fonctionnalités non demandées, juste « au cas ou ».
Pour paraphraser une partie de l'introduction du livre \emph{Clean Architecture} \cite{clean_architecture}: Pour paraphraser une partie de l'introduction du livre \emph{Clean Architecture} \cite{clean_architecture}:
\begin{quote} \begin{quote}
Getting software right is hard: it takes knowledge and skills that most young programmers don't take the time to develop. Getting software right is hard: it takes knowledge and skills that most young programmers don't take the time to develop.
It requires a level of discipline and dedication that most programmers never dreamed they'd need. It requires a level of discipline and dedication that most programmers never dreamed they'd need.
Mostly, it takes a passion for the craft and the desire to be a professional. Mostly, it takes a passion for the craft and the desire to be a professional.
--- Robert C. Martin Clean Architecture --- Robert C. Martin Clean Architecture
\end{quote} \end{quote}
Le développement d'un logiciel nécessite une rigueur d'exécution et des connaissances précises dans des domaines extrêmement variés. Le développement d'un logiciel nécessite une rigueur d'exécution et des connaissances précises dans des domaines extrêmement variés.
Il nécessite également des intentions, des (bonnes) décisions et énormément d'attention. Il nécessite également des intentions, des (bonnes) décisions et énormément d'attention.
Indépendamment de l'architecture que vous aurez choisie, des technologies que vous aurez patiemment évaluées et mises en place, une architecture et une solution peuvent être cassées en un instant, en même temps que tout ce que vous aurez construit, dès que vous en aurez détourné le regard. Indépendamment de l'architecture que vous aurez choisie, des technologies que vous aurez patiemment évaluées et mises en place, une architecture et une solution peuvent être cassées en un instant, en même temps que tout ce que vous aurez construit, dès que vous en aurez détourné le regard.
Un des objectifs ici est de placer les barrières et les gardes-fous (ou Un des objectifs ici est de placer les barrières et les gardes-fous (ou plutôt, les "\textbf{garde-vous}"), afin de péréniser au maximum les acquis, stabiliser les bases de tous les environnements (du développement à la production) qui accueilliront notre application et fiabiliser ainsi chaque étape de communication.
plutôt, les "\textbf{garde-vous}"), afin de péréniser au maximum les
acquis, stabiliser les bases de tous les environnements (du
développement à la production) qui accueilliront notre application et
fiabiliser ainsi chaque étape de communication.
Dans cette partie-ci, nous parlerons de \textbf{méthodes de travail}, Dans cette partie-ci, nous parlerons de \textbf{méthodes de travail}, avec comme objectif d'éviter que l'application ne tourne que sur notre machine et que chaque déploiement ne soit une plaie à gérer.
avec comme objectif d'éviter que l'application ne tourne que sur notre Chaque mise à jour doit être réalisable de la manière la plus simple possible, et chaque étape doit être rendue la plus automatisée/automatisable possible.
machine et que chaque déploiement ne soit une plaie à gérer. Chaque mise Dans son plus simple élément, une application pourrait être mise à jour simplement en envoyant son code sur un dépôt centralisé: ce déclencheur doit démarrer une chaîne de vérification d'utilisabilité/fonctionnalités/débuggabilité/sécurité, pour immédiatement la mettre à disposition de nouveaux utilisateurs si toute la chaîne indique que tout est OK.
à jour doit être réalisable de la manière la plus simple possible, et D'autres mécanismes fonctionnent également, mais au plus les actions nécessitent d'actions humaines, voire d'intervenants humains, au plus la probabilité qu'un problème survienne est grande.
chaque étape doit être rendue la plus automatisée/automatisable
possible. Dans son plus simple élément, une application pourrait être
mise à jour simplement en envoyant son code sur un dépôt centralisé: ce
déclencheur doit démarrer une chaîne de vérification
d'utilisabilité/fonctionnalités/débuggabilité/sécurité, pour
immédiatement la mettre à disposition de nouveaux utilisateurs si toute
la chaîne indique que tout est OK. D'autres mécanismes fonctionnent
également, mais au plus les actions nécessitent d'actions humaines,
voire d'intervenants humains, au plus la probabilité qu'un problème
survienne est grande.
Dans une version plus manuelle, cela pourrait se résumer à ces trois Dans une version plus manuelle, cela pourrait se résumer à ces trois étapes (la dernière étant formellement facultative) :
étapes (la dernière étant formellement facultative):
\begin{enumerate} \begin{enumerate}
\item \item
Démarrer un script, Démarrer un script,
\item \item
Prévoir un rollback si cela plante (et si cela a planté, préparer un Prévoir un rollback si cela plante (et si cela a planté, préparer un post-mortem de l'incident pour qu'il ne se produise plus)
post-mortem de l'incident pour qu'il ne se produise plus)
\item \item
Se préparer une tisane en regardant nos flux RSS (pour peu que cette Se préparer une tisane en regardant nos flux RSS (pour peu que cette technologie existe encore\ldots).
technologie existe encore\ldots\hspace{0pt}).
\end{enumerate} \end{enumerate}
Sans aller jusqu'à demander de développer vos algorithmes sur douze pieds, la programmation reste un art régit par un ensemble de bonnes pratiques, par des règles à respecter et par la nécessité de travailler avec d'autres personnes qui ont souvent une expérience, des compétences ou une approche différente. Sans aller jusqu'à demander de développer vos algorithmes sur douze pieds, la programmation reste un art régit par un ensemble de bonnes pratiques, par des règles à respecter et par la nécessité de travailler avec d'autres personnes qui ont souvent une expérience, des compétences ou une approche différente.

View File

@ -1,40 +1,36 @@
\part{Principes fondamentaux de Django} \part{Principes fondamentaux de Django}
Dans cette partie, nous allons parler de plusieurs concepts fondamentaux au développement rapide d'une application utilisant 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. 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 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.
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é. 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 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.
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}.
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 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}. dans les fichiers \texttt{urls.py}.
\begin{center} \begin{center}
\begin{tabular}{ |c|c|c| } \begin{tabular}{ |c|c|c| }
\hline \hline
tableau comparatif MVC vs MTV & cell2 & cell3 \\ tableau comparatif MVC vs MTV & cell2 & cell3 \\
cell4 & cell5 & cell6 \\ cell4 & cell5 & cell6 \\
cell7 & cell8 & cell9 \\ cell7 & cell8 & cell9 \\
\hline \hline
\end{tabular} \end{tabular}
\end{center} \end{center}
\begin{itemize} \begin{itemize}
\item \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. 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.
\emph{Grosso modo}*, une table SQL correspondra à une classe d'un modèle Django. \item
\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.
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. \item
En d'autres mots, la vue sert de pont entre les données gérées par la base et l'interface utilisateur. 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.
\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} \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 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. 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. Cette fonction correspond au \textbf{contrôleur} et s'occupera de construire le \textbf{modèle} correspondant.

View File

@ -6,16 +6,16 @@ Nous avons fait exprès de reprendre l'acronyme d'une \emph{Services Oriented Ar
Dans cette partie, en page, la définition d'une interface REST, la définition d'une interface GraphQL et le routage d'URLs. Dans cette partie, en page, la définition d'une interface REST, la définition d'une interface GraphQL et le routage d'URLs.
\begin{quote} \begin{quote}
Don't make me think, or why I switched from JS SPAs to Ruby On Rails Don't make me think, or why I switched from JS SPAs to Ruby On Rails
\url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment} \url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment}
\end{quote} \end{quote}
On a parcouru les templates et le mode "monolithique de DJango". On a parcouru les templates et le mode "monolithique de DJango".
Maintenant, on va aborder différentes options: Maintenant, on va aborder différentes options:
\begin{enumerate} \begin{enumerate}
\item Le mode "intermédiaire", qui consiste à garder tous les mécanismes internes à Django, mais à ajouter une couche de dynamisme au travers d'une (légère) couche de JavaScript ou via HTMX. \item
\item Le mode "warrior", qui consiste lui à ajouter une API, d'abord pour vos clients et utilisateurs, mais aussi pour votre propre consommation \footnote{Aussi intitulé "Eat your own dog's food"} Le mode "intermédiaire", qui consiste à garder tous les mécanismes internes à Django, mais à ajouter une couche de dynamisme au travers d'une (légère) couche de JavaScript ou via HTMX.
\item
Le mode "warrior", qui consiste lui à ajouter une API, d'abord pour vos clients et utilisateurs, mais aussi pour votre propre consommation \footnote{Aussi intitulé "Eat your own dog's food"}
\end{enumerate} \end{enumerate}