From bc92b559b3d2efb0e079799d689e86fcfc96788e Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Wed, 27 Apr 2022 19:33:47 +0200 Subject: [PATCH] Migrate SOA --- asciidoc-to-tex.tex | 1546 --------------------------------- chapters/api.tex | 301 +++++++ chapters/deployment-tools.tex | 18 + chapters/docker-compose.tex | 36 + chapters/filters.tex | 152 ++++ chapters/gwift.tex | 553 ++++++++++++ chapters/i18n.tex | 18 + chapters/new-project.tex | 127 ++- chapters/tests.tex | 50 ++ chapters/trees.tex | 2 + chapters/urls.tex | 125 +++ main.tex | 20 +- parts/go-live.tex | 1 + parts/soa.tex | 10 + 14 files changed, 1401 insertions(+), 1558 deletions(-) create mode 100644 chapters/api.tex create mode 100644 chapters/deployment-tools.tex create mode 100644 chapters/docker-compose.tex create mode 100644 chapters/filters.tex create mode 100644 chapters/gwift.tex create mode 100644 chapters/i18n.tex create mode 100644 chapters/trees.tex create mode 100644 chapters/urls.tex create mode 100644 parts/go-live.tex create mode 100644 parts/soa.tex diff --git a/asciidoc-to-tex.tex b/asciidoc-to-tex.tex index c275454..cdd0380 100644 --- a/asciidoc-to-tex.tex +++ b/asciidoc-to-tex.tex @@ -1072,722 +1072,11 @@ Sources complémentaires: Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/}) \end{itemize} -\hypertarget{_docker_compose}{% -\subsection{Docker-Compose}\label{_docker_compose}} - -(c/c Ced' - 2020-01-24) - -Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter -pour django. - -D'abords, après avoir installer docker-compose et les dépendances sous -debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root -pour utiliser docker. Ensuite, j'ai relancé mon pc car juste relancé un -shell n'a pas suffit pour que je puisse utiliser docker avec mon compte. - -Bon après c'est facile, un petit virtualenv pour cookiecutter, suivit -d'une installation du template django. Et puis j'ai suivi sans t -\url{https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html} - -Alors, il télécharge les images, fait un petit update, installe les -dépendances de dev, install les requirement pip \ldots\hspace{0pt} - -Du coup, ça prend vite de la place: image.png - -L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque -1,5 GB d'un coup. - -Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien -faire ou presque. - -La partie ci-dessous a été reprise telle quelle de -\href{https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html}{la -documentation de cookie-cutter-django}. - -le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt -source. - -On peut aussi passer par fabric, ansible, chef ou puppet. - \hypertarget{_autres_outils}{% \section{Autres outils}\label{_autres_outils}} -Voir aussi devpi, circus, uswgi, statsd. -See \url{https://mattsegal.dev/nginx-django-reverse-proxy-config.html} -\hypertarget{_ressources}{% -\section{Ressources}\label{_ressources}} - -\begin{itemize} -\item - \url{https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/} -\item - \href{https://docs.djangoproject.com/fr/3.0/howto/deployment/}{Déploiement}. -\item - Let's Encrypt ! -\end{itemize} - -Nous avons fait exprès de reprendre l'acronyme d'une \emph{Services -Oriented Architecture} pour cette partie. L'objectif est de vous mettre -la puce à l'oreille quant à la finalité du développement: que -l'utilisateur soit humain, bot automatique ou client Web, l'objectif est -de fournir des applications résilientes, disponibles et accessibles. - -Dans cette partie, nous aborderons les vues, la mise en forme, la mise -en page, la définition d'une interface REST, la définition d'une -interface GraphQL et le routage d'URLs. - -\hypertarget{_application_programming_interface}{% -\section{Application Programming -Interface}\label{_application_programming_interface}} - -\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs -Django Rest Framework - -Expliquer pourquoi une API est intéressante/primordiale/la première -chose à réaliser/le cadet de nos soucis. - -Voir peut-être aussi -\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/} - -Au niveau du modèle, nous allons partir de quelque chose de très simple: -des personnes, des contrats, des types de contrats, et un service -d'affectation. Quelque chose comme ceci: - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# models.py} - -\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ models} - - -\KeywordTok{class}\NormalTok{ People(models.Model):} -\NormalTok{ CIVILITY\_CHOICES }\OperatorTok{=}\NormalTok{ (} -\NormalTok{ (}\StringTok{"M"}\NormalTok{, }\StringTok{"Monsieur"}\NormalTok{),} -\NormalTok{ (}\StringTok{"Mme"}\NormalTok{, }\StringTok{"Madame"}\NormalTok{),} -\NormalTok{ (}\StringTok{"Dr"}\NormalTok{, }\StringTok{"Docteur"}\NormalTok{),} -\NormalTok{ (}\StringTok{"Pr"}\NormalTok{, }\StringTok{"Professeur"}\NormalTok{),} -\NormalTok{ (}\StringTok{""}\NormalTok{, }\StringTok{""}\NormalTok{)} -\NormalTok{ )} - -\NormalTok{ last\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} -\NormalTok{ first\_name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} -\NormalTok{ civility }\OperatorTok{=}\NormalTok{ models.CharField(} -\NormalTok{ max\_length}\OperatorTok{=}\DecValTok{3}\NormalTok{,} -\NormalTok{ choices}\OperatorTok{=}\NormalTok{CIVILITY\_CHOICES,} -\NormalTok{ default}\OperatorTok{=}\StringTok{""} -\NormalTok{ )} - - \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} - \ControlFlowTok{return} \StringTok{"}\SpecialCharTok{\{\}}\StringTok{, }\SpecialCharTok{\{\}}\StringTok{"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(}\VariableTok{self}\NormalTok{.last\_name, }\VariableTok{self}\NormalTok{.first\_name)} - - -\KeywordTok{class}\NormalTok{ Service(models.Model):} -\NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} - - \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} - \ControlFlowTok{return} \VariableTok{self}\NormalTok{.label} - - -\KeywordTok{class}\NormalTok{ ContractType(models.Model):} -\NormalTok{ label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} -\NormalTok{ short\_label }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{50}\NormalTok{)} - - \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} - \ControlFlowTok{return} \VariableTok{self}\NormalTok{.short\_label} - - -\KeywordTok{class}\NormalTok{ Contract(models.Model):} -\NormalTok{ people }\OperatorTok{=}\NormalTok{ models.ForeignKey(People, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)} -\NormalTok{ date\_begin }\OperatorTok{=}\NormalTok{ models.DateField()} -\NormalTok{ date\_end }\OperatorTok{=}\NormalTok{ models.DateField(blank}\OperatorTok{=}\VariableTok{True}\NormalTok{, null}\OperatorTok{=}\VariableTok{True}\NormalTok{)} -\NormalTok{ contract\_type }\OperatorTok{=}\NormalTok{ models.ForeignKey(ContractType, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)} -\NormalTok{ service }\OperatorTok{=}\NormalTok{ models.ForeignKey(Service, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)} - - \KeywordTok{def} \FunctionTok{\_\_str\_\_}\NormalTok{(}\VariableTok{self}\NormalTok{):} - \ControlFlowTok{if} \VariableTok{self}\NormalTok{.date\_end }\KeywordTok{is} \KeywordTok{not} \VariableTok{None}\NormalTok{:} - \ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, jusqu\textquotesingle{}au }\SpecialCharTok{\{\}}\StringTok{, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(} - \VariableTok{self}\NormalTok{.date\_begin,} - \VariableTok{self}\NormalTok{.date\_end,} - \VariableTok{self}\NormalTok{.service,} - \VariableTok{self}\NormalTok{.contract\_type} -\NormalTok{ )} - - \ControlFlowTok{return} \StringTok{"A partir du }\SpecialCharTok{\{\}}\StringTok{, à durée indéterminée, dans le service }\SpecialCharTok{\{\}}\StringTok{ (}\SpecialCharTok{\{\}}\StringTok{)"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(} - \VariableTok{self}\NormalTok{.date\_begin,} - \VariableTok{self}\NormalTok{.service,} - \VariableTok{self}\NormalTok{.contract\_type} -\NormalTok{ )} -\end{Highlighting} -\end{Shaded} - -\includegraphics{images/rest/models.png} - -\hypertarget{_configuration_2}{% -\section{Configuration}\label{_configuration_2}} - -La configuration des points de terminaison de notre API est relativement -touffue. Il convient de: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Configurer les sérialiseurs, càd. les champs que nous souhaitons - exposer au travers de l'API, -\item - Configurer les vues, càd le comportement de chacun des points de - terminaison, -\item - Configurer les points de terminaison eux-mêmes, càd les URLs - permettant d'accéder aux ressources. -\item - Et finalement ajouter quelques paramètres au niveau de notre - application. -\end{enumerate} - -\hypertarget{_suxe9rialiseurs}{% -\subsection{Sérialiseurs}\label{_suxe9rialiseurs}} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# serializers.py} - -\ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group} -\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ serializers} - -\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service} - - -\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):} - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ People} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)} - - -\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):} - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)} - - -\KeywordTok{class}\NormalTok{ ServiceSerializer(serializers.HyperlinkedModelSerializer):} - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ Service} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"name"}\NormalTok{,)} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_vues}{% -\subsection{Vues}\label{_vues}} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# views.py} - -\ImportTok{from}\NormalTok{ django.contrib.auth.models }\ImportTok{import}\NormalTok{ User, Group} -\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets} -\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ permissions} - -\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ People, Contract, Service} -\ImportTok{from}\NormalTok{ .serializers }\ImportTok{import}\NormalTok{ PeopleSerializer, ContractSerializer, ServiceSerializer} - - -\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):} -\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ People.objects.}\BuiltInTok{all}\NormalTok{()} -\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ PeopleSerializer} -\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]} - - -\KeywordTok{class}\NormalTok{ ContractViewSet(viewsets.ModelViewSet):} -\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Contract.objects.}\BuiltInTok{all}\NormalTok{()} -\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ContractSerializer} -\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]} - - -\KeywordTok{class}\NormalTok{ ServiceViewSet(viewsets.ModelViewSet):} -\NormalTok{ queryset }\OperatorTok{=}\NormalTok{ Service.objects.}\BuiltInTok{all}\NormalTok{()} -\NormalTok{ serializer\_class }\OperatorTok{=}\NormalTok{ ServiceSerializer} -\NormalTok{ permission\_class }\OperatorTok{=}\NormalTok{ [permissions.IsAuthenticated]} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_urls}{% -\subsection{URLs}\label{_urls}} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# urls.py} - -\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} -\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path, include} - -\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ routers} - -\ImportTok{from}\NormalTok{ core }\ImportTok{import}\NormalTok{ views} - - -\NormalTok{router }\OperatorTok{=}\NormalTok{ routers.DefaultRouter()} -\NormalTok{router.register(}\VerbatimStringTok{r"people"}\NormalTok{, views.PeopleViewSet)} -\NormalTok{router.register(}\VerbatimStringTok{r"contracts"}\NormalTok{, views.ContractViewSet)} -\NormalTok{router.register(}\VerbatimStringTok{r"services"}\NormalTok{, views.ServiceViewSet)} - -\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} -\NormalTok{ path(}\StringTok{"api/v1/"}\NormalTok{, include(router.urls)),} -\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls),} -\NormalTok{]} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_paramuxe8tres}{% -\subsection{Paramètres}\label{_paramuxe8tres}} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# settings.py} - -\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [} -\NormalTok{ ...} - \StringTok{"rest\_framework"}\NormalTok{,} -\NormalTok{ ...} -\NormalTok{]} - -\NormalTok{...} - -\NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{} - \StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,} - \StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10} -\NormalTok{\}} -\end{Highlighting} -\end{Shaded} - -A ce stade, en nous rendant sur l'URL -\texttt{http://localhost:8000/api/v1}, nous obtiendrons ceci: - -\includegraphics{images/rest/api-first-example.png} - -\hypertarget{_moduxe8les_et_relations}{% -\section{Modèles et relations}\label{_moduxe8les_et_relations}} - -Plus haut, nous avons utilisé une relation de type -\texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour -autoriser des relations entre vos instances à partir de l'API, mais il -faut reconnaître que cela reste assez limité. Pour palier à ceci, il -existe {[}plusieurs manières de représenter ces -relations{]}(\url{https://www.django-rest-framework.org/api-guide/relations/}): -soit \textbf{via} un hyperlien, comme ci-dessus, soit en utilisant les -clés primaires, soit en utilisant l'URL canonique permettant d'accéder à -la ressource. La solution la plus complète consiste à intégrer la -relation directement au niveau des données sérialisées, ce qui nous -permet de passer de ceci (au niveau des contrats): - -\begin{Shaded} -\begin{Highlighting}[] -\FunctionTok{\{} - \DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,} - \DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} - \DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} - \DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[} - \FunctionTok{\{} - \DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,} - \DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,} - \DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[} - \StringTok{"http://localhost:8000/api/v1/contracts/1/"}\OtherTok{,} - \StringTok{"http://localhost:8000/api/v1/contracts/2/"} - \OtherTok{]} - \FunctionTok{\}} - \OtherTok{]} -\FunctionTok{\}} -\end{Highlighting} -\end{Shaded} - -à ceci: - -\begin{Shaded} -\begin{Highlighting}[] -\FunctionTok{\{} - \DataTypeTok{"count"}\FunctionTok{:} \DecValTok{1}\FunctionTok{,} - \DataTypeTok{"next"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} - \DataTypeTok{"previous"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} - \DataTypeTok{"results"}\FunctionTok{:} \OtherTok{[} - \FunctionTok{\{} - \DataTypeTok{"last\_name"}\FunctionTok{:} \StringTok{"Bond"}\FunctionTok{,} - \DataTypeTok{"first\_name"}\FunctionTok{:} \StringTok{"James"}\FunctionTok{,} - \DataTypeTok{"contract\_set"}\FunctionTok{:} \OtherTok{[} - \FunctionTok{\{} - \DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2019{-}01{-}01"}\FunctionTok{,} - \DataTypeTok{"date\_end"}\FunctionTok{:} \KeywordTok{null}\FunctionTok{,} - \DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"} - \FunctionTok{\}}\OtherTok{,} - \FunctionTok{\{} - \DataTypeTok{"date\_begin"}\FunctionTok{:} \StringTok{"2009{-}01{-}01"}\FunctionTok{,} - \DataTypeTok{"date\_end"}\FunctionTok{:} \StringTok{"2021{-}01{-}01"}\FunctionTok{,} - \DataTypeTok{"service"}\FunctionTok{:} \StringTok{"http://localhost:8000/api/v1/services/1/"} - \FunctionTok{\}} - \OtherTok{]} - \FunctionTok{\}} - \OtherTok{]} -\FunctionTok{\}} -\end{Highlighting} -\end{Shaded} - -La modification se limite à \textbf{surcharger} la propriété, pour -indiquer qu'elle consiste en une instance d'un des sérialiseurs -existants. Nous passons ainsi de ceci - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):} - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)} - - -\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):} - - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ People} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)} -\end{Highlighting} -\end{Shaded} - -à ceci: - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{class}\NormalTok{ ContractSerializer(serializers.HyperlinkedModelSerializer):} - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ Contract} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"date\_begin"}\NormalTok{, }\StringTok{"date\_end"}\NormalTok{, }\StringTok{"service"}\NormalTok{)} - - -\KeywordTok{class}\NormalTok{ PeopleSerializer(serializers.HyperlinkedModelSerializer):} -\NormalTok{ contract\_set }\OperatorTok{=}\NormalTok{ ContractSerializer(many}\OperatorTok{=}\VariableTok{True}\NormalTok{, read\_only}\OperatorTok{=}\VariableTok{True}\NormalTok{)} - - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ People} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{, }\StringTok{"contract\_set"}\NormalTok{)} -\end{Highlighting} -\end{Shaded} - -Nous ne faisons donc bien que redéfinir la propriété -\texttt{contract\_set} et indiquons qu'il s'agit à présent d'une -instance de \texttt{ContractSerializer}, et qu'il est possible d'en -avoir plusieurs. C'est tout. - -\hypertarget{_filtres_et_recherches}{% -\section{Filtres et recherches}\label{_filtres_et_recherches}} - -A ce stade, nous pouvons juste récupérer des informations présentes dans -notre base de données, mais à part les parcourir, il est difficile d'en -faire quelque chose. - -Il est possible de jouer avec les URLs en définissant une nouvelle route -ou avec les paramètres de l'URL, ce qui demanderait alors de programmer -chaque cas possible - sans que le consommateur ne puisse les déduire -lui-même. Une solution élégante consiste à autoriser le consommateur à -filtrer les données, directement au niveau de l'API. Ceci peut être -fait. Il existe deux manières de restreindre l'ensemble des résultats -retournés: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Soit au travers d'une recherche, qui permet d'effectuer une recherche - textuelle, globale et par ensemble à un ensemble de champs, -\item - Soit au travers d'un filtre, ce qui permet de spécifier une valeur - précise à rechercher. -\end{enumerate} - -Dans notre exemple, la première possibilité sera utile pour rechercher -une personne répondant à un ensemble de critères. Typiquement, -\texttt{/api/v1/people/?search=raymond\ bond} ne nous donnera aucun -résultat, alors que \texttt{/api/v1/people/?search=james\ bond} nous -donnera le célèbre agent secret (qui a bien entendu un contrat chez -nous\ldots\hspace{0pt}). - -Le second cas permettra par contre de préciser que nous souhaitons -disposer de toutes les personnes dont le contrat est ultérieur à une -date particulière. - -Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de -proposer immédiatement les champs, et donc d'informer le consommateur -des possibilités: - -\includegraphics{images/rest/drf-filters-and-searches.png} - -\hypertarget{_recherches}{% -\subsection{Recherches}\label{_recherches}} - -La fonction de recherche est déjà implémentée au niveau de -Django-Rest-Framework, et aucune dépendance supplémentaire n'est -nécessaire. Au niveau du \texttt{viewset}, il suffit d'ajouter deux -informations: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{...} -\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ filters, viewsets} -\NormalTok{...} - -\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):} -\NormalTok{ ...} -\NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [filters.SearchFilter]} -\NormalTok{ search\_fields }\OperatorTok{=}\NormalTok{ [}\StringTok{"last\_name"}\NormalTok{, }\StringTok{"first\_name"}\NormalTok{]} -\NormalTok{ ...} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_filtres}{% -\subsection{Filtres}\label{_filtres}} - -Nous commençons par installer {[}le paquet -\texttt{django-filter}{]}(\url{https://www.django-rest-framework.org/api-guide/filtering/\#djangofilterbackend}) -et nous l'ajoutons parmi les applications installées: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{λ }\ExtensionTok{pip}\NormalTok{ install django{-}filter} -\ExtensionTok{Collecting}\NormalTok{ django{-}filter} - \ExtensionTok{Downloading}\NormalTok{ django\_filter{-}2.4.0{-}py3{-}none{-}any.whl (73 kB)} - \KeywordTok{|}\NormalTok{████████████████████████████████}\KeywordTok{|} \ExtensionTok{73}\NormalTok{ kB 2.6 MB/s} -\ExtensionTok{Requirement}\NormalTok{ already satisfied: Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from django{-}filter) }\KeywordTok{(}\ExtensionTok{3.1.7}\KeywordTok{)} -\ExtensionTok{Requirement}\NormalTok{ already satisfied: asgiref}\OperatorTok{\textless{}}\NormalTok{4,}\OperatorTok{\textgreater{}}\NormalTok{=3.2.10 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{3.3.1}\KeywordTok{)} -\ExtensionTok{Requirement}\NormalTok{ already satisfied: sqlparse}\OperatorTok{\textgreater{}}\NormalTok{=0.2.2 in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{0.4.1}\KeywordTok{)} -\ExtensionTok{Requirement}\NormalTok{ already satisfied: pytz in c:\textbackslash{}users\textbackslash{}fred\textbackslash{}sources\textbackslash{}.venvs\textbackslash{}rps\textbackslash{}lib\textbackslash{}site{-}packages (from Django}\OperatorTok{\textgreater{}}\NormalTok{=2.2{-}}\OperatorTok{\textgreater{}}\NormalTok{django{-}filter) }\KeywordTok{(}\ExtensionTok{2021.1}\KeywordTok{)} -\ExtensionTok{Installing}\NormalTok{ collected packages: django{-}filter} -\ExtensionTok{Successfully}\NormalTok{ installed django{-}filter{-}2.4.0} -\end{Highlighting} -\end{Shaded} - -Une fois l'installée réalisée, il reste deux choses à faire: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Ajouter \texttt{django\_filters} parmi les applications installées: -\item - Configurer la clé \texttt{DEFAULT\_FILTER\_BACKENDS} à la valeur - \texttt{{[}\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}{]}}. -\end{enumerate} - -Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier -\texttt{settings.py} de la manière suivante: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{REST\_FRAMEWORK }\OperatorTok{=}\NormalTok{ \{} - \StringTok{\textquotesingle{}DEFAULT\_PAGINATION\_CLASS\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}rest\_framework.pagination.PageNumberPagination\textquotesingle{}}\NormalTok{,} - \StringTok{\textquotesingle{}PAGE\_SIZE\textquotesingle{}}\NormalTok{: }\DecValTok{10}\NormalTok{,} - \StringTok{\textquotesingle{}DEFAULT\_FILTER\_BACKENDS\textquotesingle{}}\NormalTok{: [}\StringTok{\textquotesingle{}django\_filters.rest\_framework.DjangoFilterBackend\textquotesingle{}}\NormalTok{]} -\NormalTok{\}} -\end{Highlighting} -\end{Shaded} - -Au niveau du viewset, il convient d'ajouter ceci: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{...} -\ImportTok{from}\NormalTok{ django\_filters.rest\_framework }\ImportTok{import}\NormalTok{ DjangoFilterBackend} -\ImportTok{from}\NormalTok{ rest\_framework }\ImportTok{import}\NormalTok{ viewsets} -\NormalTok{...} - -\KeywordTok{class}\NormalTok{ PeopleViewSet(viewsets.ModelViewSet):} -\NormalTok{ ...} -\NormalTok{ filter\_backends }\OperatorTok{=}\NormalTok{ [DjangoFilterBackend]} -\NormalTok{ filterset\_fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}last\_name\textquotesingle{}}\NormalTok{,)} -\NormalTok{ ...} -\end{Highlighting} -\end{Shaded} - -A ce stade, nous avons deux problèmes: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Le champ que nous avons défini au niveau de la propriété - \texttt{filterset\_fields} exige une correspondance exacte. Ainsi, - \texttt{/api/v1/people/?last\_name=Bon} ne retourne rien, alors que - \texttt{/api/v1/people/?last\_name=Bond} nous donnera notre agent - secret préféré. -\item - Il n'est pas possible d'aller appliquer un critère de sélection sur la - propriété d'une relation. Notre exemple proposant rechercher - uniquement les relations dans le futur (ou dans le passé) tombe à - l'eau. -\end{enumerate} - -Pour ces deux points, nous allons définir un nouveau filtre, en -surchargeant une nouvelle classe dont la classe mère serait de type -\texttt{django\_filters.FilterSet}. - -TO BE CONTINUED. - -A noter qu'il existe un paquet -{[}Django-Rest-Framework-filters{]}(\url{https://github.com/philipn/django-rest-framework-filters}), -mais il est déprécié depuis Django 3.0, puisqu'il se base sur -\texttt{django.utils.six} qui n'existe à présent plus. Il faut donc le -faire à la main (ou patcher le paquet\ldots\hspace{0pt}). - -\hypertarget{_urls_et_espaces_de_noms}{% -\section{URLs et espaces de noms}\label{_urls_et_espaces_de_noms}} - -La gestion des URLs permet \textbf{grosso modo} d'assigner une adresse -paramétrée ou non à une fonction Python. La manière simple consiste à -modifier le fichier \texttt{gwift/settings.py} pour y ajouter nos -correspondances. Par défaut, le fichier ressemble à ceci: - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# gwift/urls.py} - -\ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url} -\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} - -\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} -\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),} -\NormalTok{]} -\end{Highlighting} -\end{Shaded} - -La variable \texttt{urlpatterns} associe un ensemble d'adresses à des -fonctions. Dans le fichier \textbf{nu}, seul le \textbf{pattern} -\texttt{admin} est défini, et inclut toutes les adresses qui sont -définies dans le fichier \texttt{admin.site.urls}. - -Django fonctionne avec des \textbf{expressions rationnelles} simplifiées -(des \textbf{expressions régulières} ou \textbf{regex}) pour trouver une -correspondance entre une URL et la fonction qui recevra la requête et -retournera une réponse. Nous utilisons l'expression \texttt{\^{}\$} pour -déterminer la racine de notre application, mais nous pourrions appliquer -d'autres regroupements (\texttt{/home}, -\texttt{users/\textless{}profile\_id\textgreater{}}, -\texttt{articles/\textless{}year\textgreater{}/\textless{}month\textgreater{}/\textless{}day\textgreater{}}, -\ldots\hspace{0pt}). Chaque \textbf{variable} déclarée dans l'expression -régulière sera apparenté à un paramètre dans la fonction correspondante. -Ainsi, - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# admin.site.urls.py} -\end{Highlighting} -\end{Shaded} - -Pour reprendre l'exemple où on en était resté: - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# gwift/urls.py} - -\ImportTok{from}\NormalTok{ django.conf.urls }\ImportTok{import}\NormalTok{ include, url} -\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} - -\ImportTok{from}\NormalTok{ wish }\ImportTok{import}\NormalTok{ views }\ImportTok{as}\NormalTok{ wish\_views} - -\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} -\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),} -\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, wish\_views.wishlists, name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),} -\NormalTok{]} -\end{Highlighting} -\end{Shaded} - -Dans la mesure du possible, essayez toujours de \textbf{nommer} chaque -expression. Cela permettra notamment de les retrouver au travers de la -fonction \texttt{reverse}, mais permettra également de simplifier vos -templates. - -A présent, on doit tester que l'URL racine de notre application mène -bien vers la fonction \texttt{wish\_views.wishlists}. - -Sauf que les pages \texttt{about} et \texttt{help} existent également. -Pour implémenter ce type de précédence, il faudrait implémenter les URLs -de la manière suivante: - -\begin{verbatim} -| about -| help -| -\end{verbatim} - -Mais cela signifie aussi que les utilisateurs \texttt{about} et -\texttt{help} (s'ils existent\ldots\hspace{0pt}) ne pourront jamais -accéder à leur profil. Une dernière solution serait de maintenir une -liste d'authorité des noms d'utilisateur qu'il n'est pas possible -d'utiliser. - -D'où l'importance de bien définir la séquence de déinition de ces -routes, ainsi que des espaces de noms. - -Note sur les namespaces. - -De là, découle une autre bonne pratique: l'utilisation de -\emph{breadcrumbs} -(\url{https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template}) -ou de guidelines de navigation. - -\hypertarget{_reverse}{% -\subsection{Reverse}\label{_reverse}} - -En associant un nom ou un libellé à chaque URL, il est possible de -récupérer sa \textbf{traduction}. Cela implique par contre de ne plus -toucher à ce libellé par la suite\ldots\hspace{0pt} - -Dans le fichier \texttt{urls.py}, on associe le libellé -\texttt{wishlists} à l'URL \texttt{r\textquotesingle{}\^{}\$} -(c'est-à-dire la racine du site): - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ wish.views }\ImportTok{import}\NormalTok{ WishListList} - -\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} -\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}admin/\textquotesingle{}}\NormalTok{, include(admin.site.urls)),} -\NormalTok{ url(}\VerbatimStringTok{r\textquotesingle{}\^{}$\textquotesingle{}}\NormalTok{, WishListList.as\_view(), name}\OperatorTok{=}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{),} -\NormalTok{]} -\end{Highlighting} -\end{Shaded} - -De cette manière, dans nos templates, on peut à présent construire un -lien vers la racine avec le tags suivant: - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{\textless{}a}\OtherTok{ href=}\StringTok{"\{\% url \textquotesingle{}wishlists\textquotesingle{} \%\}"}\KeywordTok{\textgreater{}}\NormalTok{\{\{ yearvar \}\} Archive}\KeywordTok{\textless{}/a\textgreater{}} -\end{Highlighting} -\end{Shaded} - -De la même manière, on peut également récupérer l'URL de destination -pour n'importe quel libellé, de la manière suivante: - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ django.core.urlresolvers }\ImportTok{import}\NormalTok{ reverse\_lazy} - -\NormalTok{wishlists\_url }\OperatorTok{=}\NormalTok{ reverse\_lazy(}\StringTok{\textquotesingle{}wishlists\textquotesingle{}}\NormalTok{)} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_i18n_l10n}{% -\section{i18n / l10n}\label{_i18n_l10n}} - -La localisation (\emph{l10n}) et l'internationalization (\emph{i18n}) -sont deux concepts proches, mais différents: - -\begin{itemize} -\item - Internationalisation: \emph{Preparing the software for localization. - Usually done by developers.} -\item - Localisation: \emph{Writing the translations and local formats. - Usually done by translators.} -\end{itemize} - -L'internationalisation est donc le processus permettant à une -application d'accepter une forme de localisation. La seconde ne va donc -pas sans la première, tandis que la première ne fait qu'autoriser la -seconde. \hypertarget{_arborescences}{% \subsection{Arborescences}\label{_arborescences}} @@ -1836,842 +1125,7 @@ seconde. \end{Highlighting} \end{Shaded} -\hypertarget{_conclusions_3}{% -\section{Conclusions}\label{_conclusions_3}} -De part son pattern \texttt{MVT}, Django ne fait pas comme les autres -frameworks. - -Pour commencer, nous allons nous concentrer sur la création d'un site ne -contenant qu'une seule application, même si en pratique le site -contiendra déjà plusieurs applications fournies pas django, comme nous -le verrons plus loin. Don't make me think, or why I switched from JS SPAs to Ruby On Rails \url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment} - -Pour prendre un exemple concret, nous allons créer un site permettant de -gérer des listes de souhaits, que nous appellerons \texttt{gwift} (pour -\texttt{GiFTs\ and\ WIshlisTs} :)). - -La première chose à faire est de définir nos besoins du point de vue de -l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur -puisse faire avec l'application. - -Ensuite, nous pourrons traduire ces besoins en fonctionnalités et -finalement effectuer le développement. - -\hypertarget{_gwift}{% -\section{Gwift}\label{_gwift}} - -\begin{figure} -\centering -\includegraphics{images/django/django-project-vs-apps-gwift.png} -\caption{Gwift} -\end{figure} - -\hypertarget{_besoins_utilisateurs}{% -\section{Besoins utilisateurs}\label{_besoins_utilisateurs}} - -Nous souhaitons développer un site où un utilisateur donné peut créer -une liste contenant des souhaits et où d'autres utilisateurs, -authentifiés ou non, peuvent choisir les souhaits à la réalisation -desquels ils souhaitent participer. - -Il sera nécessaire de s'authentifier pour : - -\begin{itemize} -\item - Créer une liste associée à l'utilisateur en cours -\item - Ajouter un nouvel élément à une liste -\end{itemize} - -Il ne sera pas nécessaire de s'authentifier pour : - -\begin{itemize} -\item - Faire une promesse d'offre pour un élément appartenant à une liste, - associée à un utilisateur. -\end{itemize} - -L'utilisateur ayant créé une liste pourra envoyer un email directement -depuis le site aux personnes avec qui il souhaite partager sa liste, cet -email contenant un lien permettant d'accéder à cette liste. - -A chaque souhait, on pourrait de manière facultative ajouter un prix. -Dans ce cas, le souhait pourrait aussi être subdivisé en plusieurs -parties, de manière à ce que plusieurs personnes puissent participer à -sa réalisation. - -Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à -dupliquer le souhait en question. - -\hypertarget{_besoins_fonctionnels}{% -\section{Besoins fonctionnels}\label{_besoins_fonctionnels}} - -\hypertarget{_gestion_des_utilisateurs}{% -\subsection{Gestion des utilisateurs}\label{_gestion_des_utilisateurs}} - -Pour gérer les utilisateurs, nous allons faire en sorte de surcharger ce -que Django propose: par défaut, on a une la possibilité de gérer des -utilisateurs (identifiés par une adresse email, un nom, un prénom, -\ldots\hspace{0pt}) mais sans plus. - -Ce qu'on peut souhaiter, c'est que l'utilisateur puisse s'authentifier -grâce à une plateforme connue (Facebook, Twitter, Google, etc.), et -qu'il puisse un minimum gérer son profil. - -\hypertarget{_gestion_des_listes}{% -\subsection{Gestion des listes}\label{_gestion_des_listes}} - -\hypertarget{_moduxe8lisation}{% -\subsubsection{Modèlisation}\label{_moduxe8lisation}} - -Les données suivantes doivent être associées à une liste: - -\begin{itemize} -\item - un identifiant -\item - un identifiant externe (un GUID, par exemple) -\item - un nom -\item - une description -\item - le propriétaire, associé à l'utilisateur qui l'aura créée -\item - une date de création -\item - une date de modification -\end{itemize} - -\hypertarget{_fonctionnalituxe9s}{% -\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s}} - -\begin{itemize} -\item - Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et - supprimer une liste dont il est le propriétaire -\item - Un utilisateur doit pouvoir associer ou retirer des souhaits à une - liste dont il est le propriétaire -\item - Il faut pouvoir accéder à une liste, avec un utilisateur authentifier - ou non, \textbf{via} son identifiant externe -\item - Il faut pouvoir envoyer un email avec le lien vers la liste, contenant - son identifiant externe -\item - L'utilisateur doit pouvoir voir toutes les listes qui lui - appartiennent -\end{itemize} - -\hypertarget{_gestion_des_souhaits}{% -\subsection{Gestion des souhaits}\label{_gestion_des_souhaits}} - -\hypertarget{_moduxe9lisation_2}{% -\subsubsection{Modélisation}\label{_moduxe9lisation_2}} - -Les données suivantes peuvent être associées à un souhait: - -\begin{itemize} -\item - un identifiant -\item - identifiant de la liste -\item - un nom -\item - une description -\item - le propriétaire -\item - une date de création -\item - une date de modification -\item - une image, afin de représenter l'objet ou l'idée -\item - un nombre (1 par défaut) -\item - un prix facultatif -\item - un nombre de part, facultatif également, si un prix est fourni. -\end{itemize} - -\hypertarget{_fonctionnalituxe9s_2}{% -\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_2}} - -\begin{itemize} -\item - Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et - supprimer un souhait dont il est le propriétaire. -\item - On ne peut créer un souhait sans liste associée -\item - Il faut pouvoir fractionner un souhait uniquement si un prix est - donné. -\item - Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié - ou non. -\item - Il faut pouvoir réaliser un souhait ou une partie seulement, avec un - utilisateur authentifié ou non. -\item - Un souhait en cours de réalisation et composé de différentes parts ne - peut plus être modifié. -\item - Un souhait en cours de réalisation ou réalisé ne peut plus être - supprimé. -\item - On peut modifier le nombre de fois qu'un souhait doit être réalisé - dans la limite des réalisations déjà effectuées. -\end{itemize} - -\hypertarget{_gestion_des_ruxe9alisations_de_souhaits}{% -\subsection{Gestion des réalisations de -souhaits}\label{_gestion_des_ruxe9alisations_de_souhaits}} - -\hypertarget{_moduxe9lisation_3}{% -\subsubsection{Modélisation}\label{_moduxe9lisation_3}} - -Les données suivantes peuvent être associées à une réalisation de -souhait: - -\begin{itemize} -\item - identifiant du souhait -\item - identifiant de l'utilisateur si connu -\item - identifiant de la personne si utilisateur non connu -\item - un commentaire -\item - une date de réalisation -\end{itemize} - -\hypertarget{_fonctionnalituxe9s_3}{% -\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_3}} - -\begin{itemize} -\item - L'utilisateur doit pouvoir voir si un souhait est réalisé, en partie - ou non. Il doit également avoir un pourcentage de complétion sur la - possibilité de réalisation de son souhait, entre 0\% et 100\%. -\item - L'utilisateur doit pouvoir voir la ou les personnes ayant réalisé un - souhait. -\item - Il y a autant de réalisation que de parts de souhait réalisées ou de - nombre de fois que le souhait est réalisé. -\end{itemize} - -\hypertarget{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}{% -\subsection{Gestion des personnes réalisants les souhaits et qui ne sont -pas -connues}\label{_gestion_des_personnes_ruxe9alisants_les_souhaits_et_qui_ne_sont_pas_connues}} - -\hypertarget{_moduxe9lisation_4}{% -\subsubsection{Modélisation}\label{_moduxe9lisation_4}} - -Les données suivantes peuvent être associées à une personne réalisant un -souhait: - -\begin{itemize} -\item - un identifiant -\item - un nom -\item - une adresse email facultative -\end{itemize} - -\hypertarget{_fonctionnalituxe9s_4}{% -\subsubsection{Fonctionnalités}\label{_fonctionnalituxe9s_4}} - -Modélisation - -L'ORM de Django permet de travailler uniquement avec une définition de -classes, et de faire en sorte que le lien avec la base de données soit -géré uniquement de manière indirecte, par Django lui-même. On peut -schématiser ce comportement par une classe = une table. - -Comme on l'a vu dans la description des fonctionnalités, on va -\textbf{grosso modo} avoir besoin des éléments suivants: - -\begin{itemize} -\item - Des listes de souhaits -\item - Des éléments qui composent ces listes -\item - Des parts pouvant composer chacun de ces éléments -\item - Des utilisateurs pour gérer tout ceci. -\end{itemize} - -Nous proposons dans un premier temps d'éluder la gestion des -utilisateurs, et de simplement se concentrer sur les fonctionnalités -principales. Cela nous donne ceci: - -\begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - code-block:: python - -\begin{verbatim} -# wish/models.py -\end{verbatim} - -\begin{verbatim} -from django.db import models -\end{verbatim} - -\begin{verbatim} -class Wishlist(models.Model): - pass -\end{verbatim} - -\begin{verbatim} -class Item(models.Model): - pass -\end{verbatim} - -\begin{verbatim} -class Part(models.Model): - pass -\end{verbatim} -\end{enumerate} - -Les classes sont créées, mais vides. Entrons dans les détails. - -Listes de souhaits - -Comme déjà décrit précédemment, les listes de souhaits peuvent -s'apparenter simplement à un objet ayant un nom et une description. Pour -rappel, voici ce qui avait été défini dans les spécifications: - -\begin{itemize} -\item - un identifiant -\item - un identifiant externe -\item - un nom -\item - une description -\item - une date de création -\item - une date de modification -\end{itemize} - -Notre classe \texttt{Wishlist} peut être définie de la manière suivante: - -\begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - code-block:: python - -\begin{verbatim} -# wish/models.py -\end{verbatim} - -\begin{verbatim} -class Wishlist(models.Model): -\end{verbatim} - -\begin{verbatim} -name = models.CharField(max_length=255) -description = models.TextField() -created_at = models.DateTimeField(auto_now_add=True) -updated_at = models.DateTimeField(auto_now=True) -external_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) -\end{verbatim} -\end{enumerate} - -Que peut-on constater? - -\begin{itemize} -\item - Que s'il n'est pas spécifié, un identifiant \texttt{id} sera - automatiquement généré et accessible dans le modèle. Si vous souhaitez - malgré tout spécifier que ce soit un champ en particulier qui devienne - la clé primaire, il suffit de l'indiquer grâce à l'attribut - \texttt{primary\_key=True}. -\item - Que chaque type de champs (\texttt{DateTimeField}, \texttt{CharField}, - \texttt{UUIDField}, etc.) a ses propres paramètres d'initialisation. - Il est intéressant de les apprendre ou de se référer à la - documentation en cas de doute. -\end{itemize} - -Au niveau de notre modélisation: - -\begin{itemize} -\item - La propriété \texttt{created\_at} est gérée automatiquement par Django - grâce à l'attribut \texttt{auto\_now\_add}: de cette manière, lors - d'un \textbf{ajout}, une valeur par défaut ("\textbf{maintenant}") - sera attribuée à cette propriété. -\item - La propriété \texttt{updated\_at} est également gérée automatique, - cette fois grâce à l'attribut \texttt{auto\_now} initialisé à - \texttt{True}: lors d'une \textbf{mise à jour}, la propriété se verra - automatiquement assigner la valeur du moment présent. Cela ne permet - évidemment pas de gérer un historique complet et ne nous dira pas - \textbf{quels champs} ont été modifiés, mais cela nous conviendra dans - un premier temps. -\item - La propriété \texttt{external\_id} est de type \texttt{UUIDField}. - Lorsqu'une nouvelle instance sera instanciée, cette propriété prendra - la valeur générée par la fonction \texttt{uuid.uuid4()}. \textbf{A - priori}, chacun des types de champs possède une propriété - \texttt{default}, qui permet d'initialiser une valeur sur une nouvelle - instance. -\end{itemize} - -Souhaits - -Nos souhaits ont besoin des propriétés suivantes: - -\begin{itemize} -\item - un identifiant -\item - l'identifiant de la liste auquel le souhait est lié -\item - un nom -\item - une description -\item - le propriétaire -\item - une date de création -\item - une date de modification -\item - une image permettant de le représenter. -\item - un nombre (1 par défaut) -\item - un prix facultatif -\item - un nombre de part facultatif, si un prix est fourni. -\end{itemize} - -Après implémentation, cela ressemble à ceci: - -\begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - code-block:: python - -\begin{verbatim} -# wish/models.py -\end{verbatim} - -\begin{verbatim} -class Wish(models.Model): -\end{verbatim} - -\begin{verbatim} -wishlist = models.ForeignKey(Wishlist) -name = models.CharField(max_length=255) -description = models.TextField() -created_at = models.DateTimeField(auto_now_add=True) -updated_at = models.DateTimeField(auto_now=True) -picture = models.ImageField() -numbers_available = models.IntegerField(default=1) -number_of_parts = models.IntegerField(null=True) -estimated_price = models.DecimalField(max_digits=19, decimal_places=2, - null=True) -\end{verbatim} -\end{enumerate} - -A nouveau, que peut-on constater ? - -\begin{itemize} -\item - Les clés étrangères sont gérées directement dans la déclaration du - modèle. Un champ de type `ForeignKey - \textless{}\url{https://docs.djangoproject.com/en/1.8/ref/models/fields/\#django.db.models.ForeignKey\%3E\%60_} - permet de déclarer une relation 1-N entre deux classes. Dans la même - veine, une relation 1-1 sera représentée par un champ de type - `OneToOneField - \textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/\%3E\%60}\emph{, - alors qu'une relation N-N utilisera un `ManyToManyField - \textless{}\url{https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/\%3E\%60}}. -\item - L'attribut \texttt{default} permet de spécifier une valeur initiale, - utilisée lors de la construction de l'instance. Cet attribut peut - également être une fonction. -\item - Pour rendre un champ optionnel, il suffit de lui ajouter l'attribut - \texttt{null=True}. -\item - Comme cité ci-dessus, chaque champ possède des attributs spécifiques. - Le champ \texttt{DecimalField} possède par exemple les attributs - \texttt{max\_digits} et \texttt{decimal\_places}, qui nous permettra - de représenter une valeur comprise entre 0 et plus d'un milliard (avec - deux chiffres décimaux). -\item - L'ajout d'un champ de type \texttt{ImageField} nécessite - l'installation de \texttt{pillow} pour la gestion des images. Nous - l'ajoutons donc à nos pré-requis, dans le fichier - \texttt{requirements/base.txt}. -\end{itemize} - -Parts - -Les parts ont besoins des propriétés suivantes: - -\begin{itemize} -\item - un identifiant -\item - identifiant du souhait -\item - identifiant de l'utilisateur si connu -\item - identifiant de la personne si utilisateur non connu -\item - un commentaire -\item - une date de réalisation -\end{itemize} - -Elles constituent la dernière étape de notre modélisation et représente -la réalisation d'un souhait. Il y aura autant de part d'un souhait que -le nombre de souhait à réaliser fois le nombre de part. - -Elles permettent à un utilisateur de participer au souhait émis par un -autre utilisateur. Pour les modéliser, une part est liée d'un côté à un -souhait, et d'autre part à un utilisateur. Cela nous donne ceci: - -\begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - code-block:: python - -\begin{verbatim} -from django.contrib.auth.models import User -\end{verbatim} - -\begin{verbatim} -class WishPart(models.Model): -\end{verbatim} - -\begin{verbatim} -wish = models.ForeignKey(Wish) -user = models.ForeignKey(User, null=True) -unknown_user = models.ForeignKey(UnknownUser, null=True) -comment = models.TextField(null=True, blank=True) -done_at = models.DateTimeField(auto_now_add=True) -\end{verbatim} -\end{enumerate} - -La classe \texttt{User} référencée au début du snippet correspond à -l'utilisateur qui sera connecté. Ceci est géré par Django. Lorsqu'une -requête est effectuée et est transmise au serveur, cette information -sera disponible grâce à l'objet \texttt{request.user}, transmis à chaque -fonction ou \textbf{Class-based-view}. C'est un des avantages d'un -framework tout intégré: il vient \textbf{batteries-included} et beaucoup -de détails ne doivent pas être pris en compte. Pour le moment, nous nous -limiterons à ceci. Par la suite, nous verrons comment améliorer la -gestion des profils utilisateurs, comment y ajouter des informations et -comment gérer les cas particuliers. - -La classe \texttt{UnknownUser} permet de représenter un utilisateur non -enregistré sur le site et est définie au point suivant. - -Utilisateurs inconnus - -\begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - todo:: je supprimerais pour que tous les utilisateurs soient gérés au - même endroit. -\end{enumerate} - -Pour chaque réalisation d'un souhait par quelqu'un, il est nécessaire de -sauver les données suivantes, même si l'utilisateur n'est pas enregistré -sur le site: - -\begin{itemize} -\item - un identifiant -\item - un nom -\item - une adresse email. Cette adresse email sera unique dans notre base de - données, pour ne pas créer une nouvelle occurence si un même - utilisateur participe à la réalisation de plusieurs souhaits. -\end{itemize} - -Ceci nous donne après implémentation: - -\begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - code-block:: python - -\begin{verbatim} -class UnkownUser(models.Model): -\end{verbatim} - -\begin{verbatim} -name = models.CharField(max_length=255) -email = models.CharField(email = models.CharField(max_length=255, unique=True) -\end{verbatim} -\end{enumerate} - -\hypertarget{_tests_unitaires_2}{% -\section{Tests unitaires}\label{_tests_unitaires_2}} - -\hypertarget{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}{% -\subsection{Pourquoi s'ennuyer à écrire des -tests?}\label{_pourquoi_sennuyer_uxe0_uxe9crire_des_tests}} - -Traduit grossièrement depuis un article sur `https://medium.com -\textless{}\url{https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d\#.kfyvxyb21\%3E\%60_}: - -\begin{verbatim} -Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont -\end{verbatim} - -\begin{verbatim} -Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi: -\end{verbatim} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Aide au design: écrire des tests avant d'écrire le code vous donnera - une meilleure perspective sur le design à appliquer aux API. -\item - Documentation (pour les développeurs): chaque description d'un test -\item - Tester votre compréhension en tant que développeur: -\item - Assurance qualité: des tests, 5. -\end{enumerate} - -\hypertarget{_why_bother_with_test_discipline}{% -\subsection{Why Bother with Test -Discipline?}\label{_why_bother_with_test_discipline}} - -Your tests are your first and best line of defense against software -defects. Your tests are more important than linting \& static analysis -(which can only find a subclass of errors, not problems with your actual -program logic). Tests are as important as the implementation itself (all -that matters is that the code meets the requirement --- how it's -implemented doesn't matter at all unless it's implemented poorly). - -Unit tests combine many features that make them your secret weapon to -application success: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Design aid: Writing tests first gives you a clearer perspective on the - ideal API design. -\item - Feature documentation (for developers): Test descriptions enshrine in - code every implemented feature requirement. -\item - Test your developer understanding: Does the developer understand the - problem enough to articulate in code all critical component - requirements? -\item - Quality Assurance: Manual QA is error prone. In my experience, it's - impossible for a developer to remember all features that need testing - after making a change to refactor, add new features, or remove - features. -\item - Continuous Delivery Aid: Automated QA affords the opportunity to - automatically prevent broken builds from being deployed to production. -\end{enumerate} - -Unit tests don't need to be twisted or manipulated to serve all of those -broad-ranging goals. Rather, it is in the essential nature of a unit -test to satisfy all of those needs. These benefits are all side-effects -of a well-written test suite with good coverage. - -\hypertarget{_what_are_you_testing}{% -\subsection{What are you testing?}\label{_what_are_you_testing}} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - What component aspect are you testing? -\item - What should the feature do? What specific behavior requirement are you - testing? -\end{enumerate} - -\hypertarget{_couverture_de_code_2}{% -\subsection{Couverture de code}\label{_couverture_de_code_2}} - -On a vu au chapitre 1 qu'il était possible d'obtenir une couverture de -code, c'est-à-dire un pourcentage. - -\hypertarget{_comment_tester}{% -\subsection{Comment tester ?}\label{_comment_tester}} - -Il y a deux manières d'écrire les tests: soit avant, soit après -l'implémentation. Oui, idéalement, les tests doivent être écrits à -l'avance. Entre nous, on ne va pas râler si vous faites l'inverse, -l'important étant que vous le fassiez. Une bonne métrique pour vérifier -l'avancement des tests est la couverture de code. - -Pour l'exemple, nous allons écrire la fonction -\texttt{percentage\_of\_completion} sur la classe \texttt{Wish}, et nous -allons spécifier les résultats attendus avant même d'implémenter son -contenu. Prenons le cas où nous écrivons la méthode avant son test: - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{class}\NormalTok{ Wish(models.Model):} - -\NormalTok{ [...]} - - \AttributeTok{@property} - \KeywordTok{def}\NormalTok{ percentage\_of\_completion(}\VariableTok{self}\NormalTok{):} - \CommentTok{"""} -\CommentTok{ Calcule le pourcentage de complétion pour un élément.} -\CommentTok{ """} -\NormalTok{ number\_of\_linked\_parts }\OperatorTok{=}\NormalTok{ WishPart.objects.}\BuiltInTok{filter}\NormalTok{(wish}\OperatorTok{=}\VariableTok{self}\NormalTok{).count()} -\NormalTok{ total }\OperatorTok{=} \VariableTok{self}\NormalTok{.number\_of\_parts }\OperatorTok{*} \VariableTok{self}\NormalTok{.numbers\_available} -\NormalTok{ percentage }\OperatorTok{=}\NormalTok{ (number\_of\_linked\_parts }\OperatorTok{/}\NormalTok{ total)} - \ControlFlowTok{return}\NormalTok{ percentage }\OperatorTok{*} \DecValTok{100} -\end{Highlighting} -\end{Shaded} - -Lancez maintenant la couverture de code. Vous obtiendrez ceci: - -\begin{verbatim} -$ coverage run --source "." src/manage.py test wish -$ coverage report - -Name Stmts Miss Branch BrPart Cover ------------------------------------------------------------------- -src\gwift\__init__.py 0 0 0 0 100% -src\gwift\settings\__init__.py 4 0 0 0 100% -src\gwift\settings\base.py 14 0 0 0 100% -src\gwift\settings\dev.py 8 0 2 0 100% -src\manage.py 6 0 2 1 88% -src\wish\__init__.py 0 0 0 0 100% -src\wish\admin.py 1 0 0 0 100% -src\wish\models.py 36 5 0 0 88% ------------------------------------------------------------------- -TOTAL 69 5 4 1 93% -\end{verbatim} - -Si vous générez le rapport HTML avec la commande \texttt{coverage\ html} -et que vous ouvrez le fichier -\texttt{coverage\_html\_report/src\_wish\_models\_py.html}, vous verrez -que les méthodes en rouge ne sont pas testées. \textbf{A contrario}, la -couverture de code atteignait \textbf{98\%} avant l'ajout de cette -nouvelle méthode. - -Pour cela, on va utiliser un fichier \texttt{tests.py} dans notre -application \texttt{wish}. \textbf{A priori}, ce fichier est créé -automatiquement lorsque vous initialisez une nouvelle application. - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase} - -\KeywordTok{class}\NormalTok{ TestWishModel(TestCase):} - \KeywordTok{def}\NormalTok{ test\_percentage\_of\_completion(}\VariableTok{self}\NormalTok{):} - \CommentTok{"""} -\CommentTok{ Vérifie que le pourcentage de complétion d\textquotesingle{}un souhait} -\CommentTok{ est correctement calculé.} - -\CommentTok{ Sur base d\textquotesingle{}un souhait, on crée quatre parts et on vérifie} -\CommentTok{ que les valeurs s\textquotesingle{}étalent correctement sur 25\%, 50\%, 75\% et 100\%.} -\CommentTok{ """} -\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist(name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake WishList\textquotesingle{}}\NormalTok{,} -\NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wishlist\textquotesingle{}}\NormalTok{)} -\NormalTok{ wishlist.save()} - -\NormalTok{ wish }\OperatorTok{=}\NormalTok{ Wish(wishlist}\OperatorTok{=}\NormalTok{wishlist,} -\NormalTok{ name}\OperatorTok{=}\StringTok{\textquotesingle{}Fake Wish\textquotesingle{}}\NormalTok{,} -\NormalTok{ description}\OperatorTok{=}\StringTok{\textquotesingle{}This is a faked wish\textquotesingle{}}\NormalTok{,} -\NormalTok{ number\_of\_parts}\OperatorTok{=}\DecValTok{4}\NormalTok{)} -\NormalTok{ wish.save()} - -\NormalTok{ part1 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part1\textquotesingle{}}\NormalTok{)} -\NormalTok{ part1.save()} - \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{25}\NormalTok{, wish.percentage\_of\_completion)} - -\NormalTok{ part2 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part2\textquotesingle{}}\NormalTok{)} -\NormalTok{ part2.save()} - \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{50}\NormalTok{, wish.percentage\_of\_completion)} - -\NormalTok{ part3 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part3\textquotesingle{}}\NormalTok{)} -\NormalTok{ part3.save()} - \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{75}\NormalTok{, wish.percentage\_of\_completion)} - -\NormalTok{ part4 }\OperatorTok{=}\NormalTok{ WishPart(wish}\OperatorTok{=}\NormalTok{wish, comment}\OperatorTok{=}\StringTok{\textquotesingle{}part4\textquotesingle{}}\NormalTok{)} -\NormalTok{ part4.save()} - \VariableTok{self}\NormalTok{.assertEqual(}\DecValTok{100}\NormalTok{, wish.percentage\_of\_completion)} -\end{Highlighting} -\end{Shaded} - -L'attribut \texttt{@property} sur la méthode -\texttt{percentage\_of\_completion()} va nous permettre d'appeler -directement la méthode \texttt{percentage\_of\_completion()} comme s'il -s'agissait d'une propriété de la classe, au même titre que les champs -\texttt{number\_of\_parts} ou \texttt{numbers\_available}. Attention que -ce type de méthode contactera la base de données à chaque fois qu'elle -sera appelée. Il convient de ne pas surcharger ces méthodes de -connexions à la base: sur de petites applications, ce type de -comportement a très peu d'impacts, mais ce n'est plus le cas sur de -grosses applications ou sur des méthodes fréquemment appelées. Il -convient alors de passer par un mécanisme de \textbf{cache}, que nous -aborderons plus loin. - -En relançant la couverture de code, on voit à présent que nous arrivons -à 99\%: - -\begin{verbatim} -$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html; -. ----------------------------------------------------------------------- -Ran 1 test in 0.006s - -OK -Creating test database for alias 'default'... -Destroying test database for alias 'default'... -Name Stmts Miss Branch BrPart Cover ------------------------------------------------------------------- -src\gwift\__init__.py 0 0 0 0 100% -src\gwift\settings\__init__.py 4 0 0 0 100% -src\gwift\settings\base.py 14 0 0 0 100% -src\gwift\settings\dev.py 8 0 2 0 100% -src\manage.py 6 0 2 1 88% -src\wish\__init__.py 0 0 0 0 100% -src\wish\admin.py 1 0 0 0 100% -src\wish\models.py 34 0 0 0 100% -src\wish\tests.py 20 0 0 0 100% ------------------------------------------------------------------- -TOTAL 87 0 4 1 99% -\end{verbatim} - -En continuant de cette manière (ie. Ecriture du code et des tests, -vérification de la couverture de code), on se fixe un objectif idéal dès -le début du projet. En prenant un développement en cours de route, -fixez-vous comme objectif de ne jamais faire baisser la couverture de -code. - -\hypertarget{_quelques_liens_utiles}{% -\subsection{Quelques liens utiles}\label{_quelques_liens_utiles}} - - - -\end{document} diff --git a/chapters/api.tex b/chapters/api.tex new file mode 100644 index 0000000..b7e53f4 --- /dev/null +++ b/chapters/api.tex @@ -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. diff --git a/chapters/deployment-tools.tex b/chapters/deployment-tools.tex new file mode 100644 index 0000000..483df97 --- /dev/null +++ b/chapters/deployment-tools.tex @@ -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} diff --git a/chapters/docker-compose.tex b/chapters/docker-compose.tex new file mode 100644 index 0000000..9e42147 --- /dev/null +++ b/chapters/docker-compose.tex @@ -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. \ No newline at end of file diff --git a/chapters/filters.tex b/chapters/filters.tex new file mode 100644 index 0000000..15881fb --- /dev/null +++ b/chapters/filters.tex @@ -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}). diff --git a/chapters/gwift.tex b/chapters/gwift.tex new file mode 100644 index 0000000..1126669 --- /dev/null +++ b/chapters/gwift.tex @@ -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} \ No newline at end of file diff --git a/chapters/i18n.tex b/chapters/i18n.tex new file mode 100644 index 0000000..30a527b --- /dev/null +++ b/chapters/i18n.tex @@ -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. diff --git a/chapters/new-project.tex b/chapters/new-project.tex index ce81a12..a1ac758 100644 --- a/chapters/new-project.tex +++ b/chapters/new-project.tex @@ -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. diff --git a/chapters/tests.tex b/chapters/tests.tex index f475eef..6fcce97 100644 --- a/chapters/tests.tex +++ b/chapters/tests.tex @@ -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. diff --git a/chapters/trees.tex b/chapters/trees.tex new file mode 100644 index 0000000..c5467f5 --- /dev/null +++ b/chapters/trees.tex @@ -0,0 +1,2 @@ +\chapter{Arborescences} + diff --git a/chapters/urls.tex b/chapters/urls.tex new file mode 100644 index 0000000..baae6d5 --- /dev/null +++ b/chapters/urls.tex @@ -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 +| +\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} +{{ yearvar }} Archive +\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} \ No newline at end of file diff --git a/main.tex b/main.tex index 9f282fb..5662f5e 100644 --- a/main.tex +++ b/main.tex @@ -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} diff --git a/parts/go-live.tex b/parts/go-live.tex new file mode 100644 index 0000000..67b936f --- /dev/null +++ b/parts/go-live.tex @@ -0,0 +1 @@ +\part{Go Live!} diff --git a/parts/soa.tex b/parts/soa.tex new file mode 100644 index 0000000..da3adc1 --- /dev/null +++ b/parts/soa.tex @@ -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.