Migrate SOA
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-04-27 19:33:47 +02:00
parent 373a39a22b
commit bc92b559b3
14 changed files with 1401 additions and 1558 deletions

File diff suppressed because it is too large Load Diff

301
chapters/api.tex Normal file
View File

@ -0,0 +1,301 @@
\chapter{Application Programming Interface}
\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs
Django Rest Framework
Expliquer pourquoi une API est intéressante/primordiale/la première
chose à réaliser/le cadet de nos soucis.
Voir peut-être aussi
\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/}
Au niveau du modèle, nous allons partir de quelque chose de très simple:
des personnes, des contrats, des types de contrats, et un service
d'affectation. Quelque chose comme ceci:
\begin{minted}{python}
# models.py
from django.db import models
class People(models.Model):
CIVILITY_CHOICES = (
("M", "Monsieur"),
("Mme", "Madame"),
("Dr", "Docteur"),
("Pr", "Professeur"),
("", "")
)
last_name = models.CharField(max_length=255)
first_name = models.CharField(max_length=255)
civility = models.CharField(
max_length=3,
choices=CIVILITY_CHOICES,
default=""
)
def __str__(self):
return "{}, {}".format(self.last_name, self.first_name)
class Service(models.Model):
label = models.CharField(max_length=255)
def __str__(self):
return self.label
class ContractType(models.Model):
label = models.CharField(max_length=255)
short_label = models.CharField(max_length=50)
def __str__(self):
return self.short_label
class Contract(models.Model):
people = models.ForeignKey(People, on_delete=models.CASCADE)
date_begin = models.DateField()
date_end = models.DateField(blank=True, null=True)
contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE)
service = models.ForeignKey(Service, on_delete=models.CASCADE)
def __str__(self):
if self.date_end is not None:
return "A partir du {}, jusqu'au {}, dans le service {} ({})".format(
self.date_begin,
self.date_end,
self.service,
self.contract_type
)
return "A partir du {}, à durée indéterminée, dans le service {}({})".format(
self.date_begin,
self.service,
self.contract_type
)
\end{minted}
\includegraphics{images/rest/models.png}
\section{Mise en place}
La configuration des points de terminaison de notre API est relativement
touffue. Il convient de:
\begin{enumerate}
\item
Configurer les sérialiseurs, càd. les champs que nous souhaitons
exposer au travers de l'API,
\item
Configurer les vues, càd le comportement de chacun des points de
terminaison,
\item
Configurer les points de terminaison eux-mêmes, càd les URLs
permettant d'accéder aux ressources.
\item
Et finalement ajouter quelques paramètres au niveau de notre
application.
\end{enumerate}
\subsection{Serialiseurs}
\begin{minted}{python}
# serializers.py
from django.contrib.auth.models import User, Group
from rest_framework import serializers
from .models import People, Contract, Service
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class ServiceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Service
fields = ("name",)
\end{minted}
\subsection{Vues}
\begin{minted}{python}
# views.py
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from rest_framework import permissions
from .models import People, Contract, Service
from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer
class PeopleViewSet(viewsets.ModelViewSet):
queryset = People.objects.all()
serializer_class = PeopleSerializer
permission_class = [permissions.IsAuthenticated]
class ContractViewSet(viewsets.ModelViewSet):
queryset = Contract.objects.all()
serializer_class = ContractSerializer
permission_class = [permissions.IsAuthenticated]
class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all()
serializer_class = ServiceSerializer
permission_class = [permissions.IsAuthenticated]
\end{minted}
\subsection{URLs}
\begin{minted}{python}
# urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from core import views
router = routers.DefaultRouter()
router.register(r"people", views.PeopleViewSet)
router.register(r"contracts", views.ContractViewSet)
router.register(r"services", views.ServiceViewSet)
urlpatterns = [
path("api/v1/", include(router.urls)),
path('admin/', admin.site.urls),
]
\end{minted}
\begin{minted}{python}
# settings.py
INSTALLED_APPS = [
...
"rest_framework",
...
]
...
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS':
'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
\end{minted}
\subsection{Résultat}
En nous rendant sur l'URL \texttt{http://localhost:8000/api/v1}, nous obtiendrons ceci:
\includegraphics{images/rest/api-first-example.png}
\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
\url{https://www.django-rest-framework.org/api-guide/relations/}:
\begin{enumerate}
\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.
\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):
\begin{minted}{js}
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
"http://localhost:8000/api/v1/contracts/1/",
"http://localhost:8000/api/v1/contracts/2/"
]
}
]
}
\end{minted}
à ceci:
\begin{minted}{js}
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
{
"date_begin": "2019-01-01",
"date_end": null,
"service": "http://localhost:8000/api/v1/services/1/"
},
{
"date_begin": "2009-01-01",
"date_end": "2021-01-01",
"service": "http://localhost:8000/api/v1/services/1/"
}
]
}
]
}
\end{minted}
La modification se limite à \textbf{surcharger} la propriété, pour
indiquer qu'elle consiste en une instance d'un des sérialiseurs
existants. Nous passons ainsi de ceci
\begin{minted}{python}
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
\end{minted}
à ceci:
\begin{minted}{python}
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
contract_set = ContractSerializer(many=True, read_only=True)
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
\end{minted}
Nous ne faisons donc bien que redéfinir la propriété
\texttt{contract\_set} et indiquons qu'il s'agit à présent d'une
instance de \texttt{ContractSerializer}, et qu'il est possible d'en
avoir plusieurs. C'est tout.

View File

@ -0,0 +1,18 @@
\chapter{Autres outils}
Voir aussi devpi, circus, uswgi, statsd.
See \url{https://mattsegal.dev/nginx-django-reverse-proxy-config.html}
\hypertarget{_ressources}{%
\section{Ressources}\label{_ressources}}
\begin{itemize}
\item
\url{https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/}
\item
\href{https://docs.djangoproject.com/fr/3.0/howto/deployment/}{Déploiement}.
\item
Let's Encrypt !
\end{itemize}

View File

@ -0,0 +1,36 @@
\chapter{Docker-Compose}
(c/c Ced' - 2020-01-24)
Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter
pour django.
D'abords, après avoir installer docker-compose et les dépendances sous
debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root
pour utiliser docker. Ensuite, j'ai relancé mon pc car juste relancé un
shell n'a pas suffit pour que je puisse utiliser docker avec mon compte.
Bon après c'est facile, un petit virtualenv pour cookiecutter, suivit
d'une installation du template django. Et puis j'ai suivi sans t
\url{https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html}
Alors, il télécharge les images, fait un petit update, installe les
dépendances de dev, install les requirement pip \ldots\hspace{0pt}
Du coup, ça prend vite de la place: image.png
L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque
1,5 GB d'un coup.
Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien
faire ou presque.
La partie ci-dessous a été reprise telle quelle de
\href{https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html}{la
documentation de cookie-cutter-django}.
le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt
source.
On peut aussi passer par fabric, ansible, chef ou puppet.

152
chapters/filters.tex Normal file
View File

@ -0,0 +1,152 @@
\chapter{Filtres}
A ce stade, nous pouvons juste récupérer des informations présentes dans
notre base de données, mais à part les parcourir, il est difficile d'en
faire quelque chose.
Il est possible de jouer avec les URLs en définissant une nouvelle route
ou avec les paramètres de l'URL, ce qui demanderait alors de programmer
chaque cas possible - sans que le consommateur ne puisse les déduire
lui-même. Une solution élégante consiste à autoriser le consommateur à
filtrer les données, directement au niveau de l'API. Ceci peut être
fait. Il existe deux manières de restreindre l'ensemble des résultats
retournés:
\begin{enumerate}
\item
Soit au travers d'une recherche, qui permet d'effectuer une recherche
textuelle, globale et par ensemble à un ensemble de champs,
\item
Soit au travers d'un filtre, ce qui permet de spécifier une valeur
précise à rechercher.
\end{enumerate}
Dans notre exemple, la première possibilité sera utile pour rechercher
une personne répondant à un ensemble de critères. Typiquement,
\texttt{/api/v1/people/?search=raymond\ bond} ne nous donnera aucun
résultat, alors que \texttt{/api/v1/people/?search=james\ bond} nous
donnera le célèbre agent secret (qui a bien entendu un contrat chez
nous\ldots\hspace{0pt}).
Le second cas permettra par contre de préciser que nous souhaitons
disposer de toutes les personnes dont le contrat est ultérieur à une
date particulière.
Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de
proposer immédiatement les champs, et donc d'informer le consommateur
des possibilités:
\includegraphics{images/rest/drf-filters-and-searches.png}
\section{Recherches}
La fonction de recherche est déjà implémentée au niveau de
Django-Rest-Framework, et aucune dépendance supplémentaire n'est
nécessaire. Au niveau du \texttt{viewset}, il suffit d'ajouter deux
informations:
\begin{minted}{python}
...
from rest_framework import filters, viewsets
...
class PeopleViewSet(viewsets.ModelViewSet):
...
filter_backends = [filters.SearchFilter]
search_fields = ["last_name", "first_name"]
...
\end{minted}
\subsection{Filtres}
Nous commençons par installer le paquet django-filter \url{https://www.django-rest-framework.org/api-guide/filtering/\#djangofilterbackend})
et nous l'ajoutons parmi les applications installées:
\begin{verbatim}
pip install django-filter
Collecting django-filter
Downloading django_filter-2.4.0-py3-none-any.whl (73 kB)
| 73 kB 2.6 MB/s
Requirement already satisfied: Django>=2.2 in c:\users\fred\sources\.venvs\r
ps\lib\site-packages (from django-filter) (3.1.7)
Requirement already satisfied: asgiref<4,>=3.2.10 in c:\users\fred\sources
\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (3.3.1)
Requirement already satisfied: sqlparse>=0.2.2 in c:\users\fred\sources\.
venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (0.4.1)
Requirement already satisfied: pytz in c:\users\fred\sources\.venvs\rps\lib
\site-packages (from Django>=2.2->django-filter) (2021.1)
Installing collected packages: django-filter
Successfully installed django-filter-2.4.0
\end{verbatim}
Une fois l'installation réalisée, il reste deux choses à faire:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Ajouter \texttt{django\_filters} parmi les applications installées:
\item
Configurer la clé \texttt{DEFAULT\_FILTER\_BACKENDS} à la valeur
\texttt{{[}\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}{]}}.
\end{enumerate}
Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier
\texttt{settings.py} de la manière suivante:
\begin{minted}{python}
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS':
'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS':
['django_filters.rest_framework.DjangoFilterBackend']
}
\end{minted}
Au niveau du viewset, il convient d'ajouter ceci:
\begin{minted}{python}
...
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
...
class PeopleViewSet(viewsets.ModelViewSet):
...
filter_backends = [DjangoFilterBackend]
filterset_fields = ('last_name',)
...
\end{minted}
A ce stade, nous avons deux problèmes:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le champ que nous avons défini au niveau de la propriété
\texttt{filterset\_fields} exige une correspondance exacte. Ainsi,
\texttt{/api/v1/people/?last\_name=Bon} ne retourne rien, alors que
\texttt{/api/v1/people/?last\_name=Bond} nous donnera notre agent
secret préféré.
\item
Il n'est pas possible d'aller appliquer un critère de sélection sur la
propriété d'une relation. Notre exemple proposant rechercher
uniquement les relations dans le futur (ou dans le passé) tombe à
l'eau.
\end{enumerate}
Pour ces deux points, nous allons définir un nouveau filtre, en
surchargeant une nouvelle classe dont la classe mère serait de type
\texttt{django\_filters.FilterSet}.
TO BE CONTINUED.
A noter qu'il existe un paquet
{[}Django-Rest-Framework-filters{]}(\url{https://github.com/philipn/django-rest-framework-filters}),
mais il est déprécié depuis Django 3.0, puisqu'il se base sur
\texttt{django.utils.six} qui n'existe à présent plus. Il faut donc le
faire à la main (ou patcher le paquet\ldots\hspace{0pt}).

553
chapters/gwift.tex Normal file
View File

@ -0,0 +1,553 @@
\chapter{Gwift}
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-gwift.png}
\caption{Gwift}
\end{figure}
Pour prendre un exemple concret, nous allons créer un site permettant de
gérer des listes de souhaits, que nous appellerons \texttt{gwift} (pour
\texttt{GiFTs\ and\ WIshlisTs} :)).
La première chose à faire est de définir nos besoins du point de vue de
l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur
puisse faire avec l'application.
Ensuite, nous pourrons traduire ces besoins en fonctionnalités et
finalement effectuer le développement.
\section{Besoins utilisateurs}
Nous souhaitons développer un site où un utilisateur donné peut créer
une liste contenant des souhaits et où d'autres utilisateurs,
authentifiés ou non, peuvent choisir les souhaits à la réalisation
desquels ils souhaitent participer.
Il sera nécessaire de s'authentifier pour :
\begin{itemize}
\item
Créer une liste associée à l'utilisateur en cours
\item
Ajouter un nouvel élément à une liste
\end{itemize}
Il ne sera pas nécessaire de s'authentifier pour :
\begin{itemize}
\item
Faire une promesse d'offre pour un élément appartenant à une liste,
associée à un utilisateur.
\end{itemize}
L'utilisateur ayant créé une liste pourra envoyer un email directement
depuis le site aux personnes avec qui il souhaite partager sa liste, cet
email contenant un lien permettant d'accéder à cette liste.
A chaque souhait, on pourrait de manière facultative ajouter un prix.
Dans ce cas, le souhait pourrait aussi être subdivisé en plusieurs
parties, de manière à ce que plusieurs personnes puissent participer à
sa réalisation.
Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à
dupliquer le souhait en question.
\section{Besoins fonctionnels}
\subsection{Gestion des utilisateurs}
Pour gérer les utilisateurs, nous allons faire en sorte de surcharger ce
que Django propose: par défaut, on a une la possibilité de gérer des
utilisateurs (identifiés par une adresse email, un nom, un prénom,
\ldots\hspace{0pt}) mais sans plus.
Ce qu'on peut souhaiter, c'est que l'utilisateur puisse s'authentifier
grâce à une plateforme connue (Facebook, Twitter, Google, etc.), et
qu'il puisse un minimum gérer son profil.
\subsection{Gestion des listes}
\subsubsection{Modélisation}
Les données suivantes doivent être associées à une liste:
\begin{itemize}
\item
un identifiant
\item
un identifiant externe (un GUID, par exemple)
\item
un nom
\item
une description
\item
le propriétaire, associé à l'utilisateur qui l'aura créée
\item
une date de création
\item
une date de modification
\end{itemize}
\subsubsection{Fonctionnalités}
\begin{itemize}
\item
Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et
supprimer une liste dont il est le propriétaire
\item
Un utilisateur doit pouvoir associer ou retirer des souhaits à une
liste dont il est le propriétaire
\item
Il faut pouvoir accéder à une liste, avec un utilisateur authentifier
ou non, \textbf{via} son identifiant externe
\item
Il faut pouvoir envoyer un email avec le lien vers la liste, contenant
son identifiant externe
\item
L'utilisateur doit pouvoir voir toutes les listes qui lui
appartiennent
\end{itemize}
\subsection{Gestion des souhaits}
\subsubsection{Modélisation}
Les données suivantes peuvent être associées à un souhait:
\begin{itemize}
\item
un identifiant
\item
identifiant de la liste
\item
un nom
\item
une description
\item
le propriétaire
\item
une date de création
\item
une date de modification
\item
une image, afin de représenter l'objet ou l'idée
\item
un nombre (1 par défaut)
\item
un prix facultatif
\item
un nombre de part, facultatif également, si un prix est fourni.
\end{itemize}
\subsubsection{Fonctionnalités}
\begin{itemize}
\item
Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et
supprimer un souhait dont il est le propriétaire.
\item
On ne peut créer un souhait sans liste associée
\item
Il faut pouvoir fractionner un souhait uniquement si un prix est
donné.
\item
Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié
ou non.
\item
Il faut pouvoir réaliser un souhait ou une partie seulement, avec un
utilisateur authentifié ou non.
\item
Un souhait en cours de réalisation et composé de différentes parts ne
peut plus être modifié.
\item
Un souhait en cours de réalisation ou réalisé ne peut plus être
supprimé.
\item
On peut modifier le nombre de fois qu'un souhait doit être réalisé
dans la limite des réalisations déjà effectuées.
\end{itemize}
\subsection{Réalisation d'un souhait}
\subsubsection{Modélisation}
Les données suivantes peuvent être associées à une réalisation de
souhait:
\begin{itemize}
\item
identifiant du souhait
\item
identifiant de l'utilisateur si connu
\item
identifiant de la personne si utilisateur non connu
\item
un commentaire
\item
une date de réalisation
\end{itemize}
\subsubsection{Fonctionnalités}
\begin{itemize}
\item
L'utilisateur doit pouvoir voir si un souhait est réalisé, en partie
ou non. Il doit également avoir un pourcentage de complétion sur la
possibilité de réalisation de son souhait, entre 0\% et 100\%.
\item
L'utilisateur doit pouvoir voir la ou les personnes ayant réalisé un
souhait.
\item
Il y a autant de réalisation que de parts de souhait réalisées ou de
nombre de fois que le souhait est réalisé.
\end{itemize}
\section{Modélisation}
L'ORM de Django permet de travailler uniquement avec une définition de
classes, et de faire en sorte que le lien avec la base de données soit
géré uniquement de manière indirecte, par Django lui-même. On peut
schématiser ce comportement par une classe = une table.
Comme on l'a vu dans la description des fonctionnalités, on va
\textbf{grosso modo} avoir besoin des éléments suivants:
\begin{itemize}
\item
Des listes de souhaits
\item
Des éléments qui composent ces listes
\item
Des parts pouvant composer chacun de ces éléments
\item
Des utilisateurs pour gérer tout ceci.
\end{itemize}
Nous proposons dans un premier temps d'éluder la gestion des
utilisateurs, et de simplement se concentrer sur les fonctionnalités
principales. Cela nous donne ceci:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
# wish/models.py
\end{verbatim}
\begin{verbatim}
from django.db import models
\end{verbatim}
\begin{verbatim}
class Wishlist(models.Model):
pass
\end{verbatim}
\begin{verbatim}
class Item(models.Model):
pass
\end{verbatim}
\begin{verbatim}
class Part(models.Model):
pass
\end{verbatim}
\end{enumerate}
Les classes sont créées, mais vides. Entrons dans les détails.
Listes de souhaits
Comme déjà décrit précédemment, les listes de souhaits peuvent
s'apparenter simplement à un objet ayant un nom et une description. Pour
rappel, voici ce qui avait été défini dans les spécifications:
\begin{itemize}
\item
un identifiant
\item
un identifiant externe
\item
un nom
\item
une description
\item
une date de création
\item
une date de modification
\end{itemize}
Notre classe \texttt{Wishlist} peut être définie de la manière suivante:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
# wish/models.py
\end{verbatim}
\begin{verbatim}
class Wishlist(models.Model):
\end{verbatim}
\begin{verbatim}
name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
external_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
\end{verbatim}
\end{enumerate}
Que peut-on constater?
\begin{itemize}
\item
Que s'il n'est pas spécifié, un identifiant \texttt{id} sera
automatiquement généré et accessible dans le modèle. Si vous souhaitez
malgré tout spécifier que ce soit un champ en particulier qui devienne
la clé primaire, il suffit de l'indiquer grâce à l'attribut
\texttt{primary\_key=True}.
\item
Que chaque type de champs (\texttt{DateTimeField}, \texttt{CharField},
\texttt{UUIDField}, etc.) a ses propres paramètres d'initialisation.
Il est intéressant de les apprendre ou de se référer à la
documentation en cas de doute.
\end{itemize}
Au niveau de notre modélisation:
\begin{itemize}
\item
La propriété \texttt{created\_at} est gérée automatiquement par Django
grâce à l'attribut \texttt{auto\_now\_add}: de cette manière, lors
d'un \textbf{ajout}, une valeur par défaut ("\textbf{maintenant}")
sera attribuée à cette propriété.
\item
La propriété \texttt{updated\_at} est également gérée automatique,
cette fois grâce à l'attribut \texttt{auto\_now} initialisé à
\texttt{True}: lors d'une \textbf{mise à jour}, la propriété se verra
automatiquement assigner la valeur du moment présent. Cela ne permet
évidemment pas de gérer un historique complet et ne nous dira pas
\textbf{quels champs} ont été modifiés, mais cela nous conviendra dans
un premier temps.
\item
La propriété \texttt{external\_id} est de type \texttt{UUIDField}.
Lorsqu'une nouvelle instance sera instanciée, cette propriété prendra
la valeur générée par la fonction \texttt{uuid.uuid4()}. \textbf{A
priori}, chacun des types de champs possède une propriété
\texttt{default}, qui permet d'initialiser une valeur sur une nouvelle
instance.
\end{itemize}
Souhaits
Nos souhaits ont besoin des propriétés suivantes:
\begin{itemize}
\item
un identifiant
\item
l'identifiant de la liste auquel le souhait est lié
\item
un nom
\item
une description
\item
le propriétaire
\item
une date de création
\item
une date de modification
\item
une image permettant de le représenter.
\item
un nombre (1 par défaut)
\item
un prix facultatif
\item
un nombre de part facultatif, si un prix est fourni.
\end{itemize}
Après implémentation, cela ressemble à ceci:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
# wish/models.py
\end{verbatim}
\begin{verbatim}
class Wish(models.Model):
\end{verbatim}
\begin{verbatim}
wishlist = models.ForeignKey(Wishlist)
name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
picture = models.ImageField()
numbers_available = models.IntegerField(default=1)
number_of_parts = models.IntegerField(null=True)
estimated_price = models.DecimalField(max_digits=19, decimal_places=2,
null=True)
\end{verbatim}
\end{enumerate}
A nouveau, que peut-on constater ?
\begin{itemize}
\item
Les clés étrangères sont gérées directement dans la déclaration du
modèle. Un champ de type `ForeignKey
\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/models/fields/\#django.db.models.ForeignKey\%3E\%60_}
permet de déclarer une relation 1-N entre deux classes. Dans la même
veine, une relation 1-1 sera représentée par un champ de type
`OneToOneField
\textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/\%3E\%60}\emph{,
alors qu'une relation N-N utilisera un `ManyToManyField
\textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/\%3E\%60}}.
\item
L'attribut \texttt{default} permet de spécifier une valeur initiale,
utilisée lors de la construction de l'instance. Cet attribut peut
également être une fonction.
\item
Pour rendre un champ optionnel, il suffit de lui ajouter l'attribut
\texttt{null=True}.
\item
Comme cité ci-dessus, chaque champ possède des attributs spécifiques.
Le champ \texttt{DecimalField} possède par exemple les attributs
\texttt{max\_digits} et \texttt{decimal\_places}, qui nous permettra
de représenter une valeur comprise entre 0 et plus d'un milliard (avec
deux chiffres décimaux).
\item
L'ajout d'un champ de type \texttt{ImageField} nécessite
l'installation de \texttt{pillow} pour la gestion des images. Nous
l'ajoutons donc à nos pré-requis, dans le fichier
\texttt{requirements/base.txt}.
\end{itemize}
\subsection{Parts}
Les parts ont besoins des propriétés suivantes:
\begin{itemize}
\item
un identifiant
\item
identifiant du souhait
\item
identifiant de l'utilisateur si connu
\item
identifiant de la personne si utilisateur non connu
\item
un commentaire
\item
une date de réalisation
\end{itemize}
Elles constituent la dernière étape de notre modélisation et représente
la réalisation d'un souhait. Il y aura autant de part d'un souhait que
le nombre de souhait à réaliser fois le nombre de part.
Elles permettent à un utilisateur de participer au souhait émis par un
autre utilisateur. Pour les modéliser, une part est liée d'un côté à un
souhait, et d'autre part à un utilisateur. Cela nous donne ceci:
\begin{enumerate}
\item
code-block:: python
\begin{verbatim}
from django.contrib.auth.models import User
\end{verbatim}
\begin{verbatim}
class WishPart(models.Model):
\end{verbatim}
\begin{verbatim}
wish = models.ForeignKey(Wish)
user = models.ForeignKey(User, null=True)
unknown_user = models.ForeignKey(UnknownUser, null=True)
comment = models.TextField(null=True, blank=True)
done_at = models.DateTimeField(auto_now_add=True)
\end{verbatim}
\end{enumerate}
La classe \texttt{User} référencée au début du snippet correspond à
l'utilisateur qui sera connecté. Ceci est géré par Django. Lorsqu'une
requête est effectuée et est transmise au serveur, cette information
sera disponible grâce à l'objet \texttt{request.user}, transmis à chaque
fonction ou \textbf{Class-based-view}. C'est un des avantages d'un
framework tout intégré: il vient \textbf{batteries-included} et beaucoup
de détails ne doivent pas être pris en compte. Pour le moment, nous nous
limiterons à ceci. Par la suite, nous verrons comment améliorer la
gestion des profils utilisateurs, comment y ajouter des informations et
comment gérer les cas particuliers.
La classe \texttt{UnknownUser} permet de représenter un utilisateur non
enregistré sur le site et est définie au point suivant.
\subsection{Utilisateurs inconnus}
Utilisateurs inconnus
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
todo:: je supprimerais pour que tous les utilisateurs soient gérés au
même endroit.
\end{enumerate}
Pour chaque réalisation d'un souhait par quelqu'un, il est nécessaire de
sauver les données suivantes, même si l'utilisateur n'est pas enregistré
sur le site:
\begin{itemize}
\item
un identifiant
\item
un nom
\item
une adresse email. Cette adresse email sera unique dans notre base de
données, pour ne pas créer une nouvelle occurence si un même
utilisateur participe à la réalisation de plusieurs souhaits.
\end{itemize}
Ceci nous donne après implémentation:
\begin{enumerate}
\def\labelenumi{\alph{enumi}.}
\item
code-block:: python
\begin{verbatim}
class UnkownUser(models.Model):
\end{verbatim}
\begin{verbatim}
name = models.CharField(max_length=255)
email = models.CharField(email = models.CharField(max_length=255, unique=True)
\end{verbatim}
\end{enumerate}

18
chapters/i18n.tex Normal file
View File

@ -0,0 +1,18 @@
\chapter{i18n/l10n}
La localisation (\emph{l10n}) et l'internationalization (\emph{i18n})
sont deux concepts proches, mais différents:
\begin{itemize}
\item
Internationalisation: \emph{Preparing the software for localization.
Usually done by developers.}
\item
Localisation: \emph{Writing the translations and local formats.
Usually done by translators.}
\end{itemize}
L'internationalisation est donc le processus permettant à une
application d'accepter une forme de localisation. La seconde ne va donc
pas sans la première, tandis que la première ne fait qu'autoriser la
seconde.

View File

@ -494,6 +494,9 @@ La \href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startprojec
\section{Tests unitaires}
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.
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:
@ -572,7 +575,7 @@ La configuration peut se faire dans un fichier .coveragerc que vous placerez à
\end{verbatim}
\subsection{Réalisation des tests}
\subsection{Recommandations sur les tests}
En résumé, il est recommandé de:
@ -610,3 +613,125 @@ class HomeTests(TestCase):
self.assertEquals(view.func, home)
\end{minted}
\subsection{Couverture de code}
Pour l'exemple, nous allons écrire la fonction \texttt{percentage\_of\_completion} sur la classe \texttt{Wish}, et nous allons spécifier les résultats attendus avant même d'implémenter son contenu. Prenons le cas où nous écrivons la méthode avant son test:
\begin{minted}{python}
class Wish(models.Model):
[...]
@property
def percentage_of_completion(self):
"""
Calcule le pourcentage de complétion pour un élément.
"""
number_of_linked_parts = WishPart.objects.filter(wish=self).count()
total = self.number_of_parts * self.numbers_available
percentage = (number_of_linked_parts / total)
return percentage * 100
\end{minted}
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
\begin{verbatim}
$ coverage run --source "." src/manage.py test wish
$ coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 36 5 0 0 88%
------------------------------------------------------------------
TOTAL 69 5 4 1 93%
\end{verbatim}
Si vous générez le rapport HTML avec la commande \texttt{coverage\ html} et que vous ouvrez le fichier
\texttt{coverage\_html\_report/src\_wish\_models\_py.html}, vous verrez que les méthodes en rouge ne sont pas testées. \textbf{A contrario}, la couverture de code atteignait \textbf{98\%} avant l'ajout de cette
nouvelle méthode.
Pour cela, on va utiliser un fichier \texttt{tests.py} dans notre application \texttt{wish}. \textbf{A priori}, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application.
\begin{minted}{python}
from django.test import TestCase
class TestWishModel(TestCase):
def test_percentage_of_completion(self):
"""
Vérifie que le pourcentage de complétion d'un souhait
est correctement calculé.
Sur base d'un souhait, on crée quatre parts et on vérifie
que les valeurs s'étalent correctement sur 25%, 50%, 75% et 100%.
"""
wishlist = Wishlist(
name='Fake WishList',
description='This is a faked wishlist'
)
wishlist.save()
wish = Wish(
wishlist=wishlist,
name='Fake Wish',
description='This is a faked wish',
number_of_parts=4
)
wish.save()
part1 = WishPart(wish=wish, comment='part1')
part1.save()
self.assertEqual(25, wish.percentage_of_completion)
part2 = WishPart(wish=wish, comment='part2')
part2.save()
self.assertEqual(50, wish.percentage_of_completion)
part3 = WishPart(wish=wish, comment='part3')
part3.save()
self.assertEqual(75, wish.percentage_of_completion)
part4 = WishPart(wish=wish, comment='part4')
part4.save()
self.assertEqual(100, wish.percentage_of_completion)
\end{minted}
L'attribut \texttt{@property} sur la méthode \texttt{percentage\_of\_completion()} va nous permettre d'appeler directement la méthode \texttt{percentage\_of\_completion()} comme s'il s'agissait d'une propriété de la classe, au même titre que les champs \texttt{number\_of\_parts} ou \texttt{numbers\_available}. Attention que ce type de méthode contactera la base de données à chaque fois qu'elle sera appelée. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de
comportement a très peu d'impacts, mais ce n'est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de \textbf{cache}, que nous aborderons plus loin.
En relançant la couverture de code, on voit à présent que nous arrivons à 99\%:
\begin{verbatim}
$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 34 0 0 0 100%
src\wish\tests.py 20 0 0 0 100%
------------------------------------------------------------------
TOTAL 87 0 4 1 99%
\end{verbatim}
En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code.

View File

@ -9,6 +9,56 @@ Nothing within in the system depends on the tests, and the tests always depend i
-- Robert C. Martin, Clean Architecture
\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).
Unit tests combine many features that make them your secret weapon to application success:
\begin{enumerate}
\item
Design aid: Writing tests first gives you a clearer perspective on the ideal API design.
\item
Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.
\item
Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?
\item
Quality Assurance: Manual QA is error prone. In my experience, it's impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features.
\item
Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.
\end{enumerate}
Unit tests don't need to be twisted or manipulated to serve all of those broad-ranging goals. Rather, it is in the essential nature of a unit test to satisfy all of those needs. These benefits are all side-effects
of a well-written test suite with good coverage.
\begin{enumerate}
\item
What component aspect are you testing?
\item
What should the feature do? What specific behavior requirement are you testing?
\end{enumerate}
Traduit grossièrement depuis un article sur \url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_}:
\begin{verbatim}
Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont
\end{verbatim}
\begin{verbatim}
Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:
\end{verbatim}
\begin{enumerate}
\item
Aide au design: écrire des tests avant d'écrire le code vous donnera
une meilleure perspective sur le design à appliquer aux API.
\item
Documentation (pour les développeurs): chaque description d'un test
\item
Tester votre compréhension en tant que développeur:
\item
Assurance qualité: des tests, 5.
\end{enumerate}
\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.

2
chapters/trees.tex Normal file
View File

@ -0,0 +1,2 @@
\chapter{Arborescences}

125
chapters/urls.tex Normal file
View File

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

View File

@ -71,33 +71,31 @@
\include{chapters/infrastructure.tex}
\include{chapters/deployments.tex}
\include{chapters/heroku.tex}
\chapter{Outils complémentaires}
\include{chapters/deployment-tools.tex}
\chapter{Ressources}
\chapter{Conclusions}
\part{Services Oriented Applications}
\include{parts/soa.tex}
\chapter{Application Programming Interfaces}
\include{chapters/api.tex}
\include{chapters/trees.tex}
\chapter{A/B Testing}
\chapter{Modèles et relations}
\chapter{Filtres et recherches}
\include{chapters/filters.tex}
\include{chapters/urls.tex}
\chapter{URLs et espaces de noms}
\chapter{i18n / l20n}
\include{chapters/i18n.tex}
\chapter{Conclusions}
\part{Go Live!}
\chapter{Gwift}
\include{parts/go-live.tex}
\include{chapters/gwift.tex}
\include{chapters/khana.tex}
\chapter{Conclusions}

1
parts/go-live.tex Normal file
View File

@ -0,0 +1 @@
\part{Go Live!}

10
parts/soa.tex Normal file
View File

@ -0,0 +1,10 @@
\part{Services Oriented Applications}
Nous avons fait exprès de reprendre l'acronyme d'une \emph{Services Oriented Architecture} pour cette partie. L'objectif est de vous mettre
la puce à l'oreille quant à la finalité du développement: que
l'utilisateur soit humain, bot automatique ou client Web, l'objectif est
de fournir des applications résilientes, disponibles et accessibles.
Dans cette partie, nous aborderons les vues, la mise en forme, la mise
en page, la définition d'une interface REST, la définition d'une
interface GraphQL et le routage d'URLs.