diff --git a/chapters/docker-compose.tex b/chapters/docker-compose.tex index 9e42147..a7a964a 100644 --- a/chapters/docker-compose.tex +++ b/chapters/docker-compose.tex @@ -3,34 +3,26 @@ (c/c Ced' - 2020-01-24) -Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter -pour django. +Ç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. +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 +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} +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. +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. +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}. +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. +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 index 15881fb..e2fb5e3 100644 --- a/chapters/filters.tex +++ b/chapters/filters.tex @@ -1,51 +1,33 @@ \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. - -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: +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. + \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}). +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. +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: +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: +\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} ... @@ -81,20 +63,18 @@ et nous l'ajoutons parmi les applications installées: Successfully installed django-filter-2.4.0 \end{verbatim} -Une fois l'installation réalisée, il reste deux choses à faire: +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: + 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: - + 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': @@ -125,28 +105,19 @@ class PeopleViewSet(viewsets.ModelViewSet): 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. + \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}. +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}). +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/forms.tex b/chapters/forms.tex index 70b8e1e..bc363ef 100644 --- a/chapters/forms.tex +++ b/chapters/forms.tex @@ -2,44 +2,38 @@ \includegraphics{images/xkcd-327.png} - \begin{quote} - Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et - que l'ensemble reste cohérent. Il (le form) n'a pas à savoir que tu as - implémenté des closure tables dans un graph dirigé acyclique. + Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et que l'ensemble reste cohérent. + Il (le form) n'a pas à savoir que tu as implémenté des closure tables dans un graph dirigé acyclique. \end{quote} - + Ou comment valider proprement des données entrantes. -Quand on parle de \texttt{forms}, on ne parle pas uniquement de formulaires Web. -On pourrait considérer qu'il s'agit de leur objectif principal, mais on peut également voir un peu plus loin: on peut en fait voir les \texttt{forms} comme le point d'entrée pour chaque donnée arrivant dans notre application: il s'agit en quelque sorte d'un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle. +Quand on parle de \texttt{forms}, on ne parle pas uniquement de formulaires Web. +% TODO: tu as une explication (:) dans une explication (:). Je trouve pas ca très français. +On pourrait considérer qu'il s'agit de leur objectif principal, mais on peut également voir un peu plus loin : on peut en fait voir les \texttt{forms} comme le point d'entrée pour chaque donnée arrivant dans notre application : il s'agit en quelque sorte d'un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle. -L'exemple le plus simple est un fichier \texttt{.csv}: la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle. +L'exemple le plus simple est un fichier \texttt{.csv} : la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle. -Mauvaise idée. On peut proposer trois versions d'un même code, de la version simple (lecture du fichier csv et jonglage avec les indices de colonnes), puis une version plus sophistiquée (et plus lisible, à base -de \href{https://docs.python.org/3/library/csv.html\#csv.DictReader}{DictReader}), et la version + à base de form. +Mauvaise idée. +On peut proposer trois versions d'un même code, de la version simple (lecture du fichier csv et jonglage avec les indices de colonnes), puis une version plus sophistiquée (et plus lisible, à base de \href{https://docs.python.org/3/library/csv.html\#csv.DictReader}{DictReader}), et la version + à base de form. -Les données fournies par un utilisateur \textbf{doivent} \textbf{toujours} être validées avant introduction dans la base de données. +Les données fournies par un utilisateur \textbf{doivent} \textbf{toujours} être validées avant introduction dans la base de données. Notre base de données étant accessible ici par l'ORM, la solution consiste à introduire une couche supplémentaire de validation. Le flux à suivre est le suivant: \begin{enumerate} -\item - Création d'une instance grâce à un dictionnaire -\item - Validation des données et des informations reçues -\item - Traitement, si la validation a réussi. + \item Création d'une instance grâce à un dictionnaire + \item Validation des données et des informations reçues + \item Traitement, si la validation a réussi. \end{enumerate} Ils jouent également deux rôles importants: \begin{enumerate} -\item - Valider des données, en plus de celles déjà définies au niveau du modèle -\item - Contrôler le rendu à appliquer aux champs. + \item Valider des données, en plus de celles déjà définies au niveau du modèle + \item Contrôler le rendu à appliquer aux champs. \end{enumerate} Ils agissent come une glue entre l'utilisateur et la modélisation de vos structures de données. @@ -53,7 +47,7 @@ A compléter ;-) \section{Dépendance avec le modèle} -Un \textbf{form} peut dépendre d'une autre classe Django. +Un \textbf{form} peut dépendre d'une autre classe Django. Pour cela, il suffit de fixer l'attribut \texttt{model} au niveau de la \texttt{class\ Meta} dans la définition. \begin{minted}{python} @@ -68,16 +62,15 @@ class WishlistCreateForm(forms.ModelForm): fields = ('name', 'description') \end{minted} -De cette manière, notre form dépendra automatiquement des champs déjà déclarés dans la classe \texttt{Wishlist}. -Cela suit le principe de \texttt{DRY\ \textless{}don’t\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ qu’une\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ l’attribut\ \textasciigrave{}fields}, -nous pourrons nous assurer de faire évoluer le formulaire en fonction du -modèle sur lequel il se base. +% TODO: je comprends pas pourquoi il y a autant de \ dans le paragraphe. +De cette manière, notre form dépendra automatiquement des champs déjà déclarés dans la classe \texttt{Wishlist}. +Cela suit le principe de \texttt{DRY\ \textless{}don’t\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ qu’une\ modification\ ne\ pourrisse\ le\ code :\ en\ testant\ les\ deux\ champs\ présent\ dans\ l’attribut\ \textasciigrave{}fields}, nous pourrons nous assurer de faire évoluer le formulaire en fonction du modèle sur lequel il se base. \section{Rendu et affichage} -Le formulaire permet également de contrôler le rendu qui sera appliqué lors de la génération de la page. +Le formulaire permet également de contrôler le rendu qui sera appliqué lors de la génération de la page. Si les champs dépendent du modèle sur lequel se base le formulaire, ces widgets doivent être initialisés dans -l'attribut \texttt{Meta}. +l'attribut \texttt{Meta}. Sinon, ils peuvent l'être directement au niveau du champ. \subsection{Squelette par défaut} @@ -86,36 +79,27 @@ On a d'un côté le \{\{ form.as\_p \}\} ou \{\{ form.as\_table \}\}, mais il y \subsection{Crispy-forms} -Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela -permet de valider des données reçues en entrée et d'afficher (très) -facilement des formulaires à compléter par l'utilisateur. +Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. +Cela permet de valider des données reçues en entrée et d'afficher (très) facilement des formulaires à compléter par l'utilisateur. -Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu -l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, -modifier les types de contrôleurs, les placer au pixel près, -\ldots\hspace{0pt} Tout ça demande énormément de temps. Et c'est là -qu'intervient -\href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}. -Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation -et uni-form) et permet de contrôler entièrement le \textbf{layout} et la -présentation. +Par contre, c'est lourd. +Dès qu'on souhaite peaufiner un peu l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près, \ldots\hspace{0pt} Tout ça demande énormément de temps. +Et c'est là qu'intervient \href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}. +Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le \textbf{layout} et la présentation. (c/c depuis le lien ci-dessous) Pour chaque champ, crispy-forms va : \begin{itemize} -\item - utiliser le \texttt{verbose\_name} comme label. -\item - vérifier les paramètres \texttt{blank} et \texttt{null} pour savoir si - le champ est obligatoire. -\item - utiliser le type de champ pour définir le type de la balise - \texttt{\textless{}input\textgreater{}}. -\item - récupérer les valeurs du paramètre \texttt{choices} (si présent) pour - la balise \texttt{\textless{}select\textgreater{}}. + \item + utiliser le \texttt{verbose\_name} comme label. + \item + vérifier les paramètres \texttt{blank} et \texttt{null} pour savoir si le champ est obligatoire. + \item + utiliser le type de champ pour définir le type de la balise \texttt{\textless{}input\textgreater{}}. + \item + récupérer les valeurs du paramètre \texttt{choices} (si présent) pour la balise \texttt{\textless{}select\textgreater{}}. \end{itemize} \url{http://dotmobo.github.io/django-crispy-forms.html} diff --git a/chapters/gwift.tex b/chapters/gwift.tex index 1126669..1d41229 100644 --- a/chapters/gwift.tex +++ b/chapters/gwift.tex @@ -6,548 +6,397 @@ \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} :)). +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. +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. -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. +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 + \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. + \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. +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. +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. -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. -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. -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: - +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 + \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 + 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 + 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 + 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 + 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 + 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: +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. + \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. + 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 + On ne peut créer un souhait sans liste associée \item - Il faut pouvoir fractionner un souhait uniquement si un prix est - donné. + 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. + 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. + 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é. + 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é. + 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. + 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: +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 + \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\%. + 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. + 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é. + 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. - -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: +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. + \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: +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: +% TODO: il y a beaucoup de verbatim dans l'enumerate ci-dessous. \begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - code-block:: python + \def\labelenumi{\alph{enumi}.} + \item + code-block:: python -\begin{verbatim} -# wish/models.py -\end{verbatim} + \begin{verbatim} + # wish/models.py + \end{verbatim} -\begin{verbatim} -from django.db import models -\end{verbatim} + \begin{verbatim} + from django.db import models + \end{verbatim} -\begin{verbatim} -class Wishlist(models.Model): - pass -\end{verbatim} + \begin{verbatim} + class Wishlist(models.Model): + pass + \end{verbatim} -\begin{verbatim} -class Item(models.Model): - pass -\end{verbatim} + \begin{verbatim} + class Item(models.Model): + pass + \end{verbatim} -\begin{verbatim} -class Part(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. +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: +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 + \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 + \def\labelenumi{\alph{enumi}.} + \item + code-block:: python -\begin{verbatim} -# wish/models.py -\end{verbatim} + \begin{verbatim} + # wish/models.py + \end{verbatim} -\begin{verbatim} -class Wishlist(models.Model): -\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} + \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? - +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. + \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: - +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. + \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: - +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. + \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: - - - +Après implémentation, cela ressemble à ceci : \begin{enumerate} \def\labelenumi{\alph{enumi}.} \item code-block:: python - + \begin{verbatim} - # wish/models.py + # wish/models.py \end{verbatim} - + \begin{verbatim} - class Wish(models.Model): + 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) + 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. +A nouveau, que peut-on constater ? +\begin{itemize} + \item + Les clés étrangères sont gérées directement dans la déclaration dumodè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. + % TODO : il y a des fautes de syntaxe dans + 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. -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. + \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: - +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. + \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: - +Ceci nous donne après implémentation : \begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - code-block:: python + \def\labelenumi{\alph{enumi}.} + \item + code-block:: python -\begin{verbatim} -class UnkownUser(models.Model): -\end{verbatim} + \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} + \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/heroku.tex b/chapters/heroku.tex index 8cefc62..ebe2911 100644 --- a/chapters/heroku.tex +++ b/chapters/heroku.tex @@ -1,26 +1,24 @@ \chapter{PaaS - Heroku} - -\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A Service} paas, où vous choisissez le \emph{service} dont vous avez besoin (une base de données, un service de cache, un service applicatif, ..., vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l'hôte. Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d'autant plus qu'il ne sera pas -possible de modifier un fichier une fois qu'elle aura démarré: si vous souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et envoyer de nouveaux paramètres et recommencer le déploiement depuis le +\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A Service} paas, où vous choisissez le \emph{service} dont vous avez besoin (une base de données, un service de cache, un service applicatif, \ldots, vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l'hôte. +Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d'autant plus qu'il ne sera pas possible de modifier un fichier une fois qu'elle aura démarré : si vous souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et envoyer de nouveaux paramètres et recommencer le déploiement depuis le début. \begin{figure} -\centering -\includegraphics{images/deployment/heroku.png} -\caption{Invest in apps, not ops. Heroku handles the hard stuff --- -patching and upgrading, 24/7 ops and security, build systems, failovers, -and more --- so your developers can stay focused on building great -apps.} + \centering + \includegraphics{images/deployment/heroku.png} + \caption{Invest in apps, not ops. Heroku handles the hard stuff --- patching and upgrading, 24/7 ops and security, build systems, failovers, and more --- so your developers can stay focused on building great apps.} \end{figure} -Pour un projet de type "hobby" et pour l'exemple de déploiement ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un \emph{Most Valuable Product} en place. La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un \emph{bucket compatible S3}, par exemple chez Amazon, Scaleway ou OVH. +Pour un projet de type "hobby" et pour l'exemple de déploiement ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un \emph{Most Valuable Product} en place. +La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un \emph{bucket compatible S3}, par exemple chez Amazon, Scaleway ou OVH. -Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé. Il suffit donc d'envoyer les sources de votre application vers ce dépôt pour qu'Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition. Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l'équipe. Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de \emph{pipeline}, lorsque tous les tests -unitaires et d'intégration auront été réalisés. - -Au travers de la commande \texttt{heroku\ create}, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier \texttt{.git/config} ci-dessous: +Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé. +Il suffit donc d'envoyer les sources de votre application vers ce dépôt pour qu'Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition. +Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l'équipe. +Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de \emph{pipeline}, lorsque tous les tests unitaires et d'intégration auront été réalisés. +Au travers de la commande \texttt{heroku\ create}, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier \texttt{.git/config} ci-dessous : \begin{verbatim} $ heroku create Creating app... done, -> young-temple-86098 @@ -43,24 +41,20 @@ Au travers de la commande \texttt{heroku\ create}, vous associez donc une nouvel \begin{verbatim} Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration. - Celle-ci se limite aux deux fichiers suivants: - + Celle-ci se limite aux deux fichiers suivants : + * Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI * Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`) \end{verbatim} -Après ce paramétrage, il suffit de pousser les changements vers ce -nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}. +Après ce paramétrage, il suffit de pousser les changements vers ce nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}. -Heroku propose des espaces de déploiements, mais pas d'espace de -stockage. Il est possible d'y envoyer des fichiers utilisateurs -(typiquement, des media personnalisés), mais ceux-ci seront perdus lors -du redémarrage du container. Il est donc primordial de configurer -correctement l'hébergement des fichiers média, de préférences sur un -stockage compatible S3. s3 +Heroku propose des espaces de déploiements, mais pas d'espace de stockage. +Il est possible d'y envoyer des fichiers utilisateurs (typiquement, des media personnalisés), mais ceux-ci seront perdus lors du redémarrage du container. +Il est donc primordial de configurer correctement l'hébergement des fichiers média, de préférences sur un stockage compatible S3. +s3 -Prêt à vous lancer ? Commencez par créer un compte: -\url{https://signup.heroku.com/python}. +Prêt à vous lancer ? Commencez par créer un compte : \url{https://signup.heroku.com/python}. \section{Configuration du compte} @@ -69,37 +63,29 @@ Prêt à vous lancer ? Commencez par créer un compte: Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre première application; heureusement, la documentation est super bien faite: \begin{figure} -\centering -\includegraphics{images/deployment/heroku-new-app.png} -\caption{Heroku: Commencer à travailler avec un langage} + \centering + \includegraphics{images/deployment/heroku-new-app.png} + \caption{Heroku: Commencer à travailler avec un langage} \end{figure} Installez ensuite la CLI (\emph{Command Line Interface}) en suivant \href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation suivante}. -Au besoin, cette CLI existe pour: - +Au besoin, cette CLI existe pour : \begin{enumerate} -\item - macOS, \emph{via} `brew ` -\item - Windows, grâce à un - \href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la - version 32 bits existe aussi, mais il est peu probable que vous en - ayez besoin) -\item - GNU/Linux, via un script Shell - \texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh} - ou sur \href{https://snapcraft.io/heroku}{SnapCraft}. + \item + macOS, \emph{via} `brew ` + \item + Windows, grâce à un \href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la version 32 bits existe aussi, mais il est peu probable que vous en ayez besoin) + \item + GNU/Linux, via un script Shell \texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh} ou sur \href{https://snapcraft.io/heroku}{SnapCraft}. \end{enumerate} -Une fois installée, connectez-vous: - +Une fois installée, connectez-vous : \begin{verbatim} $ heroku login \end{verbatim} -Et créer votre application: - +Et créer votre application : \begin{verbatim} $ heroku create Creating app... done, -> young-temple-86098 @@ -113,12 +99,8 @@ Et créer votre application: \caption{Notre application est à présent configurée!} \end{figure} - - Ajoutons lui une base de données, que nous sauvegarderons à intervalle -régulier: - - +régulier : \begin{verbatim} $ heroku addons:create heroku-postgresql:hobby-dev Creating heroku-postgresql:hobby-dev on -> still-thicket-66406... free @@ -127,7 +109,7 @@ régulier: ! data from another database with pg:copy Created postgresql-clear-39693 as DATABASE_URL Use heroku addons:docs heroku-postgresql to view documentation - + $ heroku pg:backups schedule --at '14:00 Europe/Brussels' DATABASE_URL Scheduling automatic daily backups of postgresql-clear-39693 at 14:00 Europe/Brussels... done @@ -145,21 +127,21 @@ régulier: heroku config:set DJANGO_DEBUG=False heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)" - + # Generating a 32 character-long random string without any of the visually similar characters "IOl01": heroku config:set DJANGO_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A- HJ-NP-Za-km-z2-9' | head -c 32)/" - + # Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com' heroku config:set DJANGO_ALLOWED_HOSTS= - + # Assign with AWS_ACCESS_KEY_ID heroku config:set DJANGO_AWS_ACCESS_KEY_ID= - + # Assign with AWS_SECRET_ACCESS_KEY heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY= - + # Assign with AWS_STORAGE_BUCKET_NAME heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME= git push heroku master @@ -170,24 +152,22 @@ régulier: \section{Type d'application} -Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement. Pour un projet Django, cela comprend, à placer à la racine de votre projet: - +Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement. +Pour un projet Django, cela comprend, à placer à la racine de votre projet : \begin{enumerate} -\item - Un fichier \texttt{requirements.txt} (qui peut éventuellement faire appel à un autre fichier, \textbf{via} l'argument \texttt{-r}) -\item - Un fichier \texttt{Procfile} ({[}sans extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!), - qui expliquera la commande pour le protocole WSGI. + \item + Un fichier \texttt{requirements.txt} (qui peut éventuellement faire appel à un autre fichier, \textbf{via} l'argument \texttt{-r}) + \item + Un fichier \texttt{Procfile} ({[}sans extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!), qui expliquera la commande pour le protocole WSGI. \end{enumerate} -Dans notre exemple: - +Dans notre exemple : \begin{verbatim} -# requirements.txt -django==3.2.8 -gunicorn -boto3 -django-storages + # requirements.txt + django==3.2.8 + gunicorn + boto3 + django-storages \end{verbatim} \begin{verbatim} @@ -198,50 +178,36 @@ django-storages \section{Hébergement S3} -Pour cette partie, nous allons nous baser sur -l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de -Scaleway}. Ils offrent 75GB de stockage et de transfert par mois, ce qui -va nous laisser suffisament d'espace pour jouer un peu. +Pour cette partie, nous allons nous baser sur l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de Scaleway}. +Ils offrent 75GB de stockage et de transfert par mois, ce qui va nous laisser suffisament d'espace pour jouer un peu. \includegraphics{images/deployment/scaleway-object-storage-bucket.png} -L'idée est qu'au moment de la construction des fichiers statiques, -Django aille simplement les héberger sur un espace de stockage -compatible S3. La complexité va être de configurer correctement les -différents points de terminaison. Pour héberger nos fichiers sur notre -\textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes -dans l'ordre: +L'idée est qu'au moment de la construction des fichiers statiques, Django aille simplement les héberger sur un espace de stockage compatible S3. +La complexité va être de configurer correctement les différents points de terminaison. +Pour héberger nos fichiers sur notre \textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes dans l'ordre : \begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y - en a - \textbf{littéralement} - des dizaines. -\item - Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec - ce type de protocole -\item - Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de - faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et - Django, qui s'attend à ce qu'on lui donne un moteur de gestion - \textbf{via} la clé - {[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}). + \def\labelenumi{\arabic{enumi}.} + \item + Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y + en a - \textbf{littéralement} - des dizaines. + \item + Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec + ce type de protocole + \item + Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de + faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et + Django, qui s'attend à ce qu'on lui donne un moteur de gestion + \textbf{via} la clé + {[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}). \end{enumerate} -La première étape consiste à se rendre dans {[}la console -Scaleway{]}(\url{https://console.scaleway.com/project/credentials}), -pour gérer ses identifiants et créer un jeton. +La première étape consiste à se rendre dans {[}la console Scaleway{]}(\url{https://console.scaleway.com/project/credentials}), pour gérer ses identifiants et créer un jeton. \includegraphics{images/deployment/scaleway-api-key.png} -Selon la documentation de -\href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages}, -de -\href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3} -et de -\href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway}, -vous aurez besoin des clés suivantes au niveau du fichier -\texttt{settings.py}: +Selon la documentation de \href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages}, de \href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3} et de \href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway}, vous aurez besoin des clés suivantes au niveau du fichier \texttt{settings.py} : \begin{minted}{python} AWS_ACCESS_KEY_ID = os.getenv('ACCESS_KEY_ID') @@ -283,10 +249,8 @@ qui sera confirmée par le \textbf{bucket}: Sources complémentaires: \begin{itemize} -\item - {[}How to store Django static and media files on S3 in - production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/}) -\item - {[}Using Django and - Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/}) + \item + {[}How to store Django static and media files on S3 in production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/}) + \item + {[}Using Django and Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/}) \end{itemize} \ No newline at end of file diff --git a/chapters/i18n.tex b/chapters/i18n.tex index 30a527b..9c0937c 100644 --- a/chapters/i18n.tex +++ b/chapters/i18n.tex @@ -4,15 +4,11 @@ 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.} + \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. +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/infrastructure.tex b/chapters/infrastructure.tex index 102c8de..0bd0a8b 100644 --- a/chapters/infrastructure.tex +++ b/chapters/infrastructure.tex @@ -1,141 +1,117 @@ \chapter{Infrastructure et composants} -Pour une mise ne production, le standard \emph{de facto} est le suivant: - +Pour une mise ne production, le standard \emph{de facto} est le suivant : \begin{itemize} -\item - Nginx comme reverse proxy -\item - HAProxy pour la distribution de charge -\item - Gunicorn ou Uvicorn comme serveur d'application -\item - Supervisor pour le monitoring -\item - PostgreSQL ou MySQL/MariaDB comme bases de données. -\item - Celery et RabbitMQ pour l'exécution de tâches asynchrones -\item - Redis / Memcache pour la mise à en cache (et pour les sessions ? A - vérifier). -\item - Sentry, pour le suivi des bugs + \item Nginx comme reverse proxy + \item HAProxy pour la distribution de charge + \item Gunicorn ou Uvicorn comme serveur d'application + \item Supervisor pour le monitoring + \item PostgreSQL ou MySQL/MariaDB comme bases de données. + \item Celery et RabbitMQ pour l'exécution de tâches asynchrones + \item Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier). + \item Sentry, pour le suivi des bugs \end{itemize} -Si nous schématisons l'infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante: - +Si nous schématisons l'infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante : \begin{enumerate} -\item - L'utilisateur fait une requête via son navigateur (Firefox ou Chrome) -\item - Le navigateur envoie une requête http, sa version, un verbe (GET, - POST, \ldots\hspace{0pt}), un port et éventuellement du contenu -\item - Le firewall du serveur (Debian GNU/Linux, CentOS, ... - vérifie si la requête peut être prise en compte -\item - La requête est transmise à l'application qui écoute sur le port - (probablement 80 ou 443; et \emph{a priori} Nginx) -\item - Elle est ensuite transmise par socket et est prise en compte par un - des \emph{workers} (= un processus Python) instancié par Gunicorn. Si - l'un de ces travailleurs venait à planter, il serait automatiquement - réinstancié par Supervisord. -\item - Qui la transmet ensuite à l'un de ses \emph{workers} (= un processus - Python). -\item - Après exécution, une réponse est renvoyée à l'utilisateur. + \item + L'utilisateur fait une requête via son navigateur (Firefox ou Chrome) + \item + Le navigateur envoie une requête http, sa version, un verbe (GET, POST, \ldots\hspace{0pt}), un port et éventuellement du contenu + \item + Le firewall du serveur (Debian GNU/Linux, CentOS, \ldots~ vérifie si la requête peut être prise en compte + \item + La requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et \emph{a priori} Nginx) + \item + Elle est ensuite transmise par socket et est prise en compte par un des \emph{workers} (= un processus Python) instancié par Gunicorn. + Si l'un de ces travailleurs venait à planter, il serait automatiquement réinstancié par Supervisord. + \item + Qui la transmet ensuite à l'un de ses \emph{workers} (= un processus Python). + \item + Après exécution, une réponse est renvoyée à l'utilisateur. \end{enumerate} \includegraphics{images/diagrams/architecture.png} \section{Reverse proxy} - -Le principe du \textbf{proxy inverse} est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système. Il serait tout à fait possible de rendre notre application directement accessible depuis l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats \footnote{\url{https://fr.wikipedia.org/wiki/Proxy_inverse}} +Le principe du \textbf{proxy inverse} est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système. +Il serait tout à fait possible de rendre notre application directement accessible depuis l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats \footnote{\url{https://fr.wikipedia.org/wiki/Proxy_inverse}} \section{Load balancer} +% TODO: à écrire \section{Workers} +% TODO: à écrire \section{Supervision des processus} +% TODO: à écrire \section{Bases de données} +% TODO: à écrire \section{Tâches asynchrones} +% TODO: à écrire \section{Mise en cache} +% TODO: à écrire \section{Niveau application} - Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore \begin{enumerate} -\item - effectuer le routage des données, -\item - trouver la bonne fonction à exécuter, -\item - récupérer les données depuis la base de données, -\item - effectuer le rendu ou la conversion des données, -\item - et renvoyer une réponse à l'utilisateur. + \item effectuer le routage des données, + \item trouver la bonne fonction à exécuter, + \item récupérer les données depuis la base de données, + \item effectuer le rendu ou la conversion des données, + \item et renvoyer une réponse à l'utilisateur. \end{enumerate} -Comme nous l'avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application. Il est possible de démarrer petit, et de suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, d'ajouter un mécanisme de mise en cache, des logiciels de suivi, ... +Comme nous l'avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application. +Il est possible de démarrer petit, et de suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, d'ajouter un mécanisme de mise en cache, des logiciels de suivi, \ldots + \section{Supervision globale} - - \subsection{Journaux} - La structure des niveaux de journaux est essentielle. \begin{quote} -When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m. -Low printer toner is not an ERROR. + When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m. + Low printer toner is not an ERROR. ---- Dan North former ToughtWorks consultant + --- Dan North former ToughtWorks consultant \end{quote} \begin{itemize} -\item - \textbf{DEBUG}: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application. Généralement, ce niveau est désactivé pour une application qui passe en production, - sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement. -\item - \textbf{INFO}: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, ... -\item - \textbf{WARN}: Regroupe les informations qui pourraient potentiellement devenir des erreurs. -\item - \textbf{ERROR}: Indique les informations internes - Erreur lors de l'appel d'une API, erreur interne, ... -\item - \textbf{FATAL} (ou \textbf{EXCEPTION}): ... généralement suivie d'une terminaison du programme - Bind raté - d'un socket, etc. + \item + \textbf{DEBUG}: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application. + Généralement, ce niveau est désactivé pour une application qui passe en production, sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement. + \item + \textbf{INFO} : Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, \ldots + \item + \textbf{WARN} : Regroupe les informations qui pourraient potentiellement devenir des erreurs. + \item + \textbf{ERROR} : Indique les informations internes - Erreur lors de l'appel d'une API, erreur interne, ... + \item + \textbf{FATAL} (ou \textbf{EXCEPTION}) : \ldots généralement suivie d'une terminaison du programme - Bind raté d'un socket, etc. \end{itemize} -La configuration des \emph{loggers} est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore. Il est ainsi possible de définir des formattages, -gestionnaires (\emph{handlers}) et loggers distincts, en fonction de nos applications. - -Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements. Il n'est donc pas réellement nécessaire de chipoter la configuration, -puisque la seule classe qui va réellement nous intéresser concerne les \texttt{StreamHandler}. La configuration que nous allons utiliser est celle-ci: +La configuration des \emph{loggers} est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore. +Il est ainsi possible de définir des formattages, gestionnaires (\emph{handlers}) et loggers distincts, en fonction de nos applications. +Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements. +Il n'est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les \texttt{StreamHandler}. +La configuration que nous allons utiliser est celle-ci : \begin{enumerate} -\item - Formattage: à définir - mais la variante suivante est complète, lisible et pratique: - \texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}} -\item - Handler: juste un, qui définit un \texttt{StreamHandler} -\item - Logger: pour celui-ci, nous avons besoin d'un niveau (\texttt{level}) - et de savoir s'il faut propager les informations vers les - sous-paquets, auquel cas il nous suffira de fixer la valeur de - \texttt{propagate} à \texttt{True}. + \item + Formattage : à définir - mais la variante suivante est complète, lisible et pratique: \texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}} + \item + Handler : juste un, qui définit un \texttt{StreamHandler} + \item + Logger : pour celui-ci, nous avons besoin d'un niveau (\texttt{level}) et de savoir s'il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de \texttt{propagate} à \texttt{True}. \end{enumerate} -Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant: - +Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant : \begin{minted}{python} import logging @@ -143,31 +119,29 @@ Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant: logger.debug('helloworld') \end{minted} - -\href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par -exemples}. - +\href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par exemples}. \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item - Sentry via sentry\_sdk + Sentry via sentry\_sdk \item - Nagios + Nagios \item - LibreNMS + LibreNMS \item - Zabbix + Zabbix \end{enumerate} - -Il existe également \href{https://munin-monitoring.org}{Munin}, -\href{https://www.elastic.co}{Logstash, ElasticSearch et Kibana -(ELK-Stack)} ou \href{https://www.fluentd.org}{Fluentd}. + +Il existe également \href{https://munin-monitoring.org}{Munin}, \href{https://www.elastic.co}{Logstash, ElasticSearch et Kibana (ELK-Stack)} ou \href{https://www.fluentd.org}{Fluentd}. \subsection{Zabbix, Nagios, ...} +% TODO: a écrire \subsection{Sentry} +% TODO: a écrire \subsection{Greylog} +% TODO: a écrire diff --git a/chapters/introduction.tex b/chapters/introduction.tex index 16c0abd..be3a496 100644 --- a/chapters/introduction.tex +++ b/chapters/introduction.tex @@ -1,20 +1,15 @@ \chapter{Introduction} \begin{quote} -The only way to go fast, is to go well + The only way to go fast, is to go well ---- Robert C. Martin + --- Robert C. Martin \end{quote} -Nous n'allons pas vous mentir: il existe énormément de tutoriaux très -bien réalisés sur "\emph{Comment réaliser une application Django}" et -autres "\emph{Déployer votre code en 2 minutes}". Nous nous disions -juste que ces tutoriaux restaient relativement haut-niveaux et se -limitaient à un contexte donné, sans réellement préparer à la -maintenance et au suivi de l'application nouvellement développée. - -Les quelques idées ci-dessous de jeter les bases d'un bon développement, en +Nous n'allons pas vous mentir: il existe énormément de tutoriaux très bien réalisés sur "\emph{Comment réaliser une application Django}" et autres "\emph{Déployer votre code en 2 minutes}". +Nous nous disions juste que ces tutoriaux restaient relativement haut-niveaux et se limitaient à un contexte donné, sans réellement préparer à la maintenance et au suivi de l'application nouvellement développée. +Les quelques idées ci-dessous de jeter les bases d'un bon développement, en \begin{itemize} \item Survolant l'ensemble des lignes directrices reconnues \item Maintenant une bonne qualité de code @@ -23,163 +18,111 @@ Les quelques idées ci-dessous de jeter les bases d'un bon développement, en \item Permettant à quiconque de reprendre ce qui aura déjà été écrit. \end{itemize} -Ces idées ne s'appliquent pas uniquement à Django et à son cadre de travail, ni même au langage Python en particulier. +Ces idées ne s'appliquent pas uniquement à Django et à son cadre de travail, ni même au langage Python en particulier. Ces deux sujets sont cependant de bons candidats et leur utilisation et cadre de travail sont bien définis, documentés et flexibles. -Django se présente comme un \emph{Framework Web pour perfectionnistes -ayant des deadlines} \cite{django} et suit ces quelques principes -\cite{django_design_philosophies}: +Django se présente comme un \emph{Framework Web pour perfectionnistes ayant des deadlines} \cite{django} et suit ces quelques principes \cite{django_design_philosophies}: \begin{itemize} -\item - \textbf{Faible couplage et forte cohésion}, pour que chaque composant dispose de son indépendance, en n'ayant aucune connaissance des autres couches applicatives. Ainsi, le moteur de rendu ne connait absolument rien à l'existence du moteur de base de données, tout comme le système de vues ne sait pas quel moteur de rendu est utilisé. -\item - \textbf{Plus de fonctionnalités avec moins de code}: chaque application Django - doit utiliser le moins de code possible -\item - \textbf{\emph{Don't repeat yourself}}, chaque concept ou morceau de code ne - doit être présent qu'à un et un seul endroit de vos dépôts. -\item - \textbf{Rapidité du développement}, en masquant les aspects fastidieux du - développement web actuel + \item + \textbf{Faible couplage et forte cohésion}, pour que chaque composant dispose de son indépendance, en n'ayant aucune connaissance des autres couches applicatives. + Ainsi, le moteur de rendu ne connait absolument rien à l'existence du moteur de base de données, tout comme le système de vues ne sait pas quel moteur de rendu est utilisé. + \item + \textbf{Plus de fonctionnalités avec moins de code}: chaque application Django doit utiliser le moins de code possible + \item + \textbf{\emph{Don't repeat yourself}}, chaque concept ou morceau de code ne doit être présent qu'à un et un seul endroit de vos dépôts. + \item + \textbf{Rapidité du développement}, en masquant les aspects fastidieux du développement web actuel \end{itemize} -Mis côte à côte, le suivi de ces principes permet une bonne stabilité du -projet à moyen et long terme. +Mis côte à côte, le suivi de ces principes permet une bonne stabilité du projet à moyen et long terme. -Sans être parfait, Django offre une énorme flexibilité qui permet de conserver un maximum d'options ouvertes et de facilement expérimenter différentes pistes, jusqu'au moment de prendre une vraie décision. -Pour la (grande) majorité des problèmes rencontrés lors du développement d'une application Web, Django proposera une solution pragmatique, compréhensible et facile à mettre en place: pour tout problème communément connu, vous disposerez d'une solution logique. +Sans être parfait, Django offre une énorme flexibilité qui permet de conserver un maximum d'options ouvertes et de facilement expérimenter différentes pistes, jusqu'au moment de prendre une vraie décision. +Pour la (grande) majorité des problèmes rencontrés lors du développement d'une application Web, Django proposera une solution pragmatique, compréhensible et facile à mettre en place: pour tout problème communément connu, vous disposerez d'une solution logique. Tout pour plaire à n'importe quel directeur IT. -\textbf{Dans la première partie}, nous verrons comment partir d'un -environnement sain, comment le configurer correctement, comment -installer Django de manière isolée et comment démarrer un nouveau -projet. Nous verrons rapidement comment gérer les dépendances, les -versions et comment appliquer et suivre un score de qualité de notre -code. Ces quelques points pourront être appliqués pour n'importe quel -langage ou cadre de travail. Nous verrons aussi que la configuration -proposée par défaut par le framework n'est pas idéale dans la majorité -des cas. +\textbf{Dans la première partie}, nous verrons comment partir d'un environnement sain, comment le configurer correctement, comment installer Django de manière isolée et comment démarrer un nouveau projet. +Nous verrons rapidement comment gérer les dépendances, les versions et comment appliquer et suivre un score de qualité de notre code. +Ces quelques points pourront être appliqués pour n'importe quel langage ou cadre de travail. +Nous verrons aussi que la configuration proposée par défaut par le framework n'est pas idéale dans la majorité des cas. -Pour cela, nous présenterons différents outils, la rédaction de tests -unitaires et d'intégration pour limiter les régressions, les règles de -nomenclature et de contrôle du contenu, comment partir d'un squelette -plus complet, ainsi que les bonnes étapes à suivre pour arriver à un -déploiement rapide et fonctionnel avec peu d'efforts. +Pour cela, nous présenterons différents outils, la rédaction de tests unitaires et d'intégration pour limiter les régressions, les règles de nomenclature et de contrôle du contenu, comment partir d'un squelette plus complet, ainsi que les bonnes étapes à suivre pour arriver à un déploiement rapide et fonctionnel avec peu d'efforts. -A la fin de cette partie, vous disposerez d'un code propre et d'un -projet fonctionnel, bien qu'encore un peu inutile. +A la fin de cette partie, vous disposerez d'un code propre et d'un projet fonctionnel, bien qu'encore un peu inutile. -textbf{Dans la deuxième partie}, nous aborderons les grands principes -de modélisation, en suivant les lignes de conduites du cadre de travail. -Nous aborderons les concepts clés qui permettent à une application de -rester maintenable, les formulaires, leurs validations, comment gérer -les données en entrée, les migrations de données et l'administration. +textbf{Dans la deuxième partie}, nous aborderons les grands principes de modélisation, en suivant les lignes de conduites du cadre de travail. +Nous aborderons les concepts clés qui permettent à une application de rester maintenable, les formulaires, leurs validations, comment gérer les données en entrée, les migrations de données et l'administration. -\textbf{Dans la troisième partie}, nous détaillerons précisément les -étapes de déploiement, avec la description et la configuration de -l'infrastructure, des exemples concrets de mise à disposition sur deux -distributions principales (Debian et CentOS), sur une \emph{*Plateform -as a Service*}, ainsi que l'utilisation de Docker et Docker-Compose. +\textbf{Dans la troisième partie}, nous détaillerons précisément les étapes de déploiement, avec la description et la configuration de l'infrastructure, des exemples concrets de mise à disposition sur deux distributions principales (Debian et CentOS), sur une \emph{*Plateform as a Service*}, ainsi que l'utilisation de Docker et Docker-Compose. -Nous aborderons également la supervision et la mise à jour d'une -application existante, en respectant les bonnes pratiques -d'administration système. +Nous aborderons également la supervision et la mise à jour d'une application existante, en respectant les bonnes pratiques d'administration système. -\textbf{Dans la quatrième partie}, nous aborderons les architectures -typées \emph{entreprise}, les services et les différentes manières de -structurer notre application pour faciliter sa gestion et sa -maintenance, tout en décrivant différents types de scénarii, en fonction -des consommateurs de données. +\textbf{Dans la quatrième partie}, nous aborderons les architectures typées \emph{entreprise}, les services et les différentes manières de structurer notre application pour faciliter sa gestion et sa maintenance, tout en décrivant différents types de scénarii, en fonction des consommateurs de données. -\textbf{Dans la cinquième partie}, nous mettrons ces concepts en -pratique en présentant le développement en pratique de deux -applications, avec la description de problèmes rencontrés et la solution -qui a été choisie: définition des tables, gestion des utilisateurs, -\ldots\hspace{0pt} et mise à disposition. +\textbf{Dans la cinquième partie}, nous mettrons ces concepts en pratique en présentant le développement en pratique de deux applications, avec la description de problèmes rencontrés et la solution qui a été choisie: définition des tables, gestion des utilisateurs, \ldots\hspace{0pt} et mise à disposition. \subsection{Pour qui ?} -Avant tout, pour moi. Comme le disait le Pr Richard Feynman: "\emph{Si -vous ne savez pas expliquer quelque chose simplement, c'est que vous ne -l'avez pas compris}". \footnote{Et comme l'ajoutait Aurélie Jean dans de - L'autre côté de la machine: \emph{"Si personne ne vous pose de - questions suite à votre explication, c'est que vous n'avez pas été - suffisamment clair !"} \cite{other_side}} +Avant tout, pour moi. +Comme le disait le Pr Richard Feynman: "\emph{Si vous ne savez pas expliquer quelque chose simplement, c'est que vous ne l'avez pas compris}". +\footnote{Et comme l'ajoutait Aurélie Jean dans de L'autre côté de la machine: \emph{"Si personne ne vous pose de questions suite à votre explication, c'est que vous n'avez pas été suffisamment clair !"} \cite{other_side}} -Ce livre s'adresse autant au néophyte qui souhaite se lancer dans le -développement Web qu'à l'artisan qui a besoin d'un aide-mémoire et qui -ne se rappelle plus toujours du bon ordre des paramètres, ou à l'expert -qui souhaiterait avoir un aperçu d'une autre technologie que son domaine -privilégié de compétences. +Ce livre s'adresse autant au néophyte qui souhaite se lancer dans le développement Web qu'à l'artisan qui a besoin d'un aide-mémoire et qui ne se rappelle plus toujours du bon ordre des paramètres, ou à l'expert qui souhaiterait avoir un aperçu d'une autre technologie que son domaine privilégié de compétences. -Beaucoup de concepts présentés peuvent être oubliés ou restés inconnus -jusqu'au moment où ils seront réellement nécessaires. A ce moment-là, -pour peu que votre mémoire ait déjà entraperçu le terme, il vous sera -plus facile d'y revenir et de l'appliquer. +Beaucoup de concepts présentés peuvent être oubliés ou restés inconnus jusqu'au moment où ils seront réellement nécessaires. +A ce moment-là, pour peu que votre mémoire ait déjà entraperçu le terme, il vous sera plus facile d'y revenir et de l'appliquer. \subsection{Pour aller plus loin} -Il existe énormément de ressources, autant spécifiques à Django que plus -généralistes. Il ne sera pas possible de toutes les détailler; faites un -tour sur +Il existe énormément de ressources, autant spécifiques à Django que plus généralistes. +Il ne sera pas possible de toutes les détailler; faites un tour sur \begin{itemize} -\item - \url{https://duckduckgo.com}, -\item - \url{https://stackoverflow.com}, -\item - \url{https://ycombinator.com}, -\item - \url{https://lobste.rs/}, -\item - \url{https://lecourrierduhacker.com/} -\item - ou \url{https://www.djangoproject.com/}. + \item \url{https://duckduckgo.com}, + \item \url{https://stackoverflow.com}, + \item \url{https://ycombinator.com}, + \item \url{https://lobste.rs/}, + \item \url{https://lecourrierduhacker.com/} + \item ou \url{https://www.djangoproject.com/}. \end{itemize} -Restez curieux, ne vous enclavez pas dans une technologie en particulier -et gardez une bonne ouverture d'esprit. +Restez curieux, ne vous enclavez pas dans une technologie en particulie et gardez une bonne ouverture d'esprit. \subsection{Conventions} Les notes indiquent des anecdotes. -Les conseils indiquent des éléments utiles, mais pas spécialement -indispensables. +Les conseils indiquent des éléments utiles, mais pas spécialemen indispensables. Les notes importantes indiquent des éléments à retenir. -Ces éléments indiquent des points d'attention. Les retenir vous fera -gagner du temps en débuggage. +Ces éléments indiquent des points d'attention. +Les retenir vous fer gagner du temps en débuggage. -Les avertissements indiquent un (potentiel) danger ou des éléments -pouvant amener des conséquences pas spécialement sympathiques. +Les avertissements indiquent un (potentiel) danger ou des élément pouvant amener des conséquences pas spécialement sympathiques. Les morceaux de code source seront présentés de la manière suivante: \begin{listing}[htbp] - \begin{minted}{Python} + \begin{minted}{Python} # /. def function(param): - """ - """ - callback() - \end{minted} + """ + """ + callback() + \end{minted} \end{listing} Chaque extrait de code reprendra - \begin{itemize} - \item l'emplacement du fichier, présenté sous forme de commentaire (ligne 1), - \item Des commentaires au niveau des fonctions et méthodes, si cela s'avère nécessaire - \item Un surlignage sur les parties importantes ou récemment modifiées + \item l'emplacement du fichier, présenté sous forme de commentaire (ligne 1), + \item Des commentaires au niveau des fonctions et méthodes, si cela s'avère nécessaire + \item Un surlignage sur les parties importantes ou récemment modifiées \end{itemize} -La plupart des commandes qui seront présentées dans ce livre le seront depuis un shell sous GNU/Linux. +La plupart des commandes qui seront présentées dans ce livre le seront depuis un shell sous GNU/Linux. Certaines d'entre elles pourraient devoir être adaptées si vous utilisez un autre système d'exploitation (macOS) ou n'importe quelle autre grosse bouse commerciale. -Les morceaux de code que vous trouverez ci-dessous seront développés pour Python3.9+ et Django 3.2+. +Les morceaux de code que vous trouverez ci-dessous seront développés pour Python3.9+ et Django 3.2+. Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure. diff --git a/chapters/licence.tex b/chapters/licence.tex index 050116b..334437a 100644 --- a/chapters/licence.tex +++ b/chapters/licence.tex @@ -1,31 +1,27 @@ \chapter{Licence} -Ce travail est licencié sous Attribution-NonCommercial 4.0 International -Attribution-NonCommercial 4.0 International +Ce travail est licencié sous Attribution-NonCommercial 4.0 International Attribution-NonCommercial 4.0 International -This license requires that reusers give credit to the creator. It allows -reusers to distribute, remix, adapt, and build upon the material in any -medium or format, for noncommercial purposes only. - -You are free to: +This license requires that reusers give credit to the creator. +It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, for noncommercial purposes only. +You are free to : \begin{itemize} - \item \textbf{Share} — copy and redistribute the material in any medium or format - \item \textbf{Adapt} — remix, transform, and build upon the material + \item \textbf{Share} — copy and redistribute the material in any medium or format + \item \textbf{Adapt} — remix, transform, and build upon the material \end{itemize} Under the following terms: \begin{itemize} -\item - \textbf{Attribution}: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. -\item - \textbf{NC}: Only noncommercial use of your work is permitted. - Noncommercial means not primarily intended for or directed towards - commercial advantage or monetary compensation. + \item + \textbf{Attribution}: You must give appropriate credit, provide a link to the license, and indicate if changes were made. + You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + \item + \textbf{NC}: Only noncommercial use of your work is permitted. + Noncommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. \end{itemize} \url{https://creativecommons.org/licenses/by-nc/4.0/} -La seule exception concerne les morceaux de code (non attribués), -disponibles sous licence \href{https://mit-license.org/}{MIT}. +La seule exception concerne les morceaux de code (non attribués), disponibles sous licence \href{https://mit-license.org/}{MIT}. diff --git a/chapters/maintenability.tex b/chapters/maintenability.tex index 47e2089..b0779e1 100644 --- a/chapters/maintenability.tex +++ b/chapters/maintenability.tex @@ -1,74 +1,47 @@ \chapter{Fiabilité, évolutivité et maintenabilité} \begin{quote} -The primary cost of maintenance is in spelunking and risk - \cite[139]{clean_architecture} + The primary cost of maintenance is in spelunking and risk\cite[139]{clean_architecture} ---- Robert C. Martin + --- Robert C. Martin \end{quote} Pour la méthode de travail et de développement, nous allons nous baser sur les \href{https://12factor.net/fr/}{The Twelve-factor App} - ou plus -simplement les \textbf{12 facteurs}. -Suivre ces concepts permet de: - +simplement les \textbf{12 facteurs}. +Suivre ces concepts permet de : \begin{enumerate} -\item - \textbf{Faciliter la mise en place de phases d'automatisation}; plus - concrètement, de faciliter les mises à jour applicatives, simplifier - la gestion de l'hôte qui héberge l'application ou les services, - diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le - projet dans un processus - d'\href{https://en.wikipedia.org/wiki/Continuous_integration}{intégration - continue} ou - \href{https://en.wikipedia.org/wiki/Continuous_deployment}{déploiement - continu} -\item - \textbf{Faciliter l'intégration de nouveaux développeurs dans l'équipe ou de - personnes souhaitant rejoindre le projet}, dans la mesure où la construction d'un nouvel environnement sera grandement facilitée. -\item - \textbf{Minimiser les divergences entre les différents environnemens} - sur lesquels un projet pourrait être déployé, pour éviter de découvrir un bogue sur l'environnement de production qui serait impossible à reproduire ailleurs, simplement parce qu'un des composants varierait -\item - \textbf{Augmenter l'agilité générale du projet}, en permettant une - meilleure évolutivité architecturale et une meilleure mise à l'échelle. + \item + \textbf{Faciliter la mise en place de phases d'automatisation}; plus concrètement, de faciliter les mises à jour applicatives, simplifier la gestion de l'hôte qui héberge l'application ou les services, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'\href{https://en.wikipedia.org/wiki/Continuous_integration}{intégration continue} ou \href{https://en.wikipedia.org/wiki/Continuous_deployment}{déploiement continu} + \item + \textbf{Faciliter l'intégration de nouveaux développeurs dans l'équipe ou de personnes souhaitant rejoindre le projet}, dans la mesure où la construction d'un nouvel environnement sera grandement facilitée. + \item + \textbf{Minimiser les divergences entre les différents environnemens} sur lesquels un projet pourrait être déployé, pour éviter de découvrir un bogue sur l'environnement de production qui serait impossible à reproduire ailleurs, simplement parce qu'un des composants varierait + \item + \textbf{Augmenter l'agilité générale du projet}, en permettant une meilleure évolutivité architecturale et une meilleure mise à l'échelle. \end{enumerate} -En pratique, les points ci-dessus permettront de gagner un temps précieux à la construction d'un nouvel environnement - qu'il soit sur la machine du petit nouveau dans l'équipe, sur un serveur Azure/Heroku/Digital Ocean ou votre nouveau -Raspberry Pi Zéro planqué à la cave - et vous feront gagner un temps -précieux. +En pratique, les points ci-dessus permettront de gagner un temps précieux à la construction d'un nouvel environnement - qu'il soit sur la machine du petit nouveau dans l'équipe, sur un serveur Azure/Heroku/Digital Ocean ou votre nouveau Raspberry Pi Zéro planqué à la cave - et vous feront gagner un temps précieux. -Pour reprendre plus spécifiquement les différentes idées derrière -cette méthode, nous avons: +Pour reprendre plus spécifiquement les différentes idées derrière cette méthode, nous avons: \section{Une base de code unique, suivie par un contrôle de versions} -Chaque déploiement de l'application, et quel que soit l'environnement ciblé, se basera sur une source unique, afin de minimiser les différences que l'on pourrait trouver entre deux environnements d'un même projet. -Git est reconnu dans l'industrie comme standard des systèmes de contrôles de versions, malgré une courbe d'apprentissage assez ardue. +Chaque déploiement de l'application, et quel que soit l'environnement ciblé, se basera sur une source unique, afin de minimiser les différences que l'on pourrait trouver entre deux environnements d'un même projet. +Git est reconnu dans l'industrie comme standard des systèmes de contrôles de versions, malgré une courbe d'apprentissage assez ardue. Comme dépôt, nous pourrons par exemple utiliser GitHub, Gitea ou Gitlab, suivant que vous ayez besoin d'une plateforme centralisée, propriétaire, payante ou auto-hébergée. \index{Git} \index{Github} \index{Gitlab} \index{Gitea} \includegraphics{images/diagrams/12-factors-1.png} -Comme l'explique Eran Messeri, ingénieur dans le groupe Google Developer -Infrastructure: "Un des avantages d'utiliser un dépôt unique de sources, -est qu'il permet un accès facile et rapide à la forme la plus à jour du -code, sans aucun besoin de coordination. \cite[pp. 288-298]{devops_handbook}. -Ce cépôt n'est pas uniquement destiné à hébergé le code source, mais -également à d'autres artefacts et autres formes de connaissance: - +Comme l'explique Eran Messeri, ingénieur dans le groupe Google Developer Infrastructure: "Un des avantages d'utiliser un dépôt unique de sources, est qu'il permet un accès facile et rapide à la forme la plus à jour du code, sans aucun besoin de coordination. \cite[pp. 288-298]{devops_handbook}. +Ce cépôt n'est pas uniquement destiné à hébergé le code source, mais également à d'autres artefacts et autres formes de connaissance : \begin{itemize} -\item - Standards de configuration (Chef recipes, Puppet manifests, ... -\item - Outils de déploiement -\item - Standards de tests, y compris tout ce qui touche à la sécurité -\item - Outils de déploiement de pipeline -\item - Outils d'analyse et de monitoring -\item - Tutoriaux + \item Standards de configuration (Chef recipes, Puppet manifests, \ldots + \item Outils de déploiement + \item Standards de tests, y compris tout ce qui touche à la sécurité + \item Outils de déploiement de pipeline + \item Outils d'analyse et de monitoring + \item Tutoriaux \end{itemize} \section{Déclaration explicite et isolation des dépendances} @@ -104,17 +77,13 @@ Il faut éviter d'avoir à recompiler/redéployer l'application simplement parce que: \begin{enumerate} -\item - l'adresse du serveur de messagerie a été modifiée, -\item - un protocole a changé en cours de route -\item - la base de données a été déplacée -\item - ... + \item l'adresse du serveur de messagerie a été modifiée, + \item un protocole a changé en cours de route + \item la base de données a été déplacée + \item \ldots \end{enumerate} -En pratique, toute information susceptible d'évoluer ou de changer (un seuil, une ressource externe, un couple utilisateur/mot de passe, ...) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable. +En pratique, toute information susceptible d'évoluer ou de changer (un seuil, une ressource externe, un couple utilisateur/mot de passe, ...) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable. Ceci permet de paramétrer facilement un environnement (par exemple, un container), simplement en modifiant une variable de configuration qui spécifierait la base de données sur laquelle l'application devra se connecter. @@ -181,8 +150,8 @@ sur les \emph{releases} de Gitea, sur un serveur d'artefacts (\href{https://fr.w Toute information stockée en mémoire ou sur disque ne doit pas altérer le comportement futur de l'application, par exemple après un redémarrage non souhaité. Dit autrement, l'exécution de l'application ne doit pas dépendre de la présence d'une information stockée en mémoire ou sur disque. -Pratiquement, si l'application devait rencontrer un problème, il est nécessaire qu'elle puisse redémarrer rapidement, éventuellement en étant déployée sur un autre serveur - par exemple suite à un problème matériel. -Toute information stockée physiquement sur le premier hôte durant son exécution sur le premier hôte, puisqu'elle pourra avoir été entretemps perdue. +Pratiquement, si l'application devait rencontrer un problème, il est nécessaire qu'elle puisse redémarrer rapidement, éventuellement en étant déployée sur un autre serveur - par exemple suite à un problème matériel. +Toute information stockée physiquement sur le premier hôte durant son exécution sur le premier hôte, puisqu'elle pourra avoir été entretemps perdue. Lors d'une initialisation ou réinitialisation, la solution consiste donc à jouer sur les variables d'environnement (cf. \#3) et sur les informations que l'on pourra trouver au niveau des ressources attachées (cf \#4), et faire en sorte que les informations et données primordiales puissent être récupérées ou reconstruites. Il serait également difficile d'appliquer une mise à l'échelle de l'application, en ajoutant un nouveau serveur d'exécution, si une donnée indispensable à son fonctionnement devait se trouver sur la seule machine où elle est actuellement exécutée. @@ -202,7 +171,7 @@ Le serveur (= l'hôte) choisit d'appliquer une correspondance entre "son" port 4 \section{Connaissance et confiance des processys systèmes} -Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus \emph{stateless} (sans état). Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des particuliers pour certaines tâches: requêtes HTTP \emph{via} des processus Web; \emph{long-running} jobs pour des processus asynchrones, ... +Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus \emph{stateless} (sans état). Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des particuliers pour certaines tâches: requêtes HTTP \emph{via} des processus Web; \emph{long-running} jobs pour des processus asynchrones, ... Si cela existe sur l'hôte hébergeant l'application, ne vous fatiguez pas: utilisez le. \includegraphics{images/12factors/process-types.png} @@ -211,12 +180,12 @@ Si cela existe sur l'hôte hébergeant l'application, ne vous fatiguez pas: util Par "arrêt élégant", nous voulons surtout éviter le fameux \texttt{kill\ -9\ \textless{}pid\textgreater{}} (ou équivalent), ou tout autre arrêt brutal d'un processus qui nécessiterait une intervention urgente du -superviseur. -En prévoyant une manière élégante d'envoyer un signal de terminaison, +superviseur. +En prévoyant une manière élégante d'envoyer un signal de terminaison, \begin{enumerate} - \item Les requêtes en cours peuvent se terminer au mieux, - \item Le démarrage rapide de nouveaux processus améliorera la balance d'un processus en cours d'extinction vers des processus tout frais, en autorisant l'exécution parallèle d'anciens et de nouveaux "types" de processus + \item Les requêtes en cours peuvent se terminer au mieux, + \item Le démarrage rapide de nouveaux processus améliorera la balance d'un processus en cours d'extinction vers des processus tout frais, en autorisant l'exécution parallèle d'anciens et de nouveaux "types" de processus \end{enumerate} L'intégration de ces mécanismes dès les premières étapes de @@ -232,25 +201,25 @@ Conserver les différents environnements aussi similaires que possible, et limiter les divergences entre un environnement de développement et de production -L'exemple donné est un développeur qui utilise macOS, NGinx et SQLite, tandis que l'environnement de production tourne sur une CentOS avec Apache2 et PostgreSQL. +L'exemple donné est un développeur qui utilise macOS, NGinx et SQLite, tandis que l'environnement de production tourne sur une CentOS avec Apache2 et PostgreSQL. Faire en sorte que tous les environnements soient les plus similaires possibles limite les divergences entre environnements, facilite les déploiements et limite la casse et la découverte de modules non compatibles, au plus proche de la phase de développement, selon le principe de la corde d'Andon \cite[p. 140]{devops_handbook} \index{Andon} \footnote{Pour donner un exemple tout bête, SQLite utilise un -\href{https://www.sqlite.org/datatype3.html}{mécanisme de stockage dynamique}, associée à la valeur plutôt qu'au schéma, \emph{via} un système d'affinités. Un autre moteur de base de données définira un schéma statique et rigide, où la valeur sera déterminée par son contenant. -Un champ \texttt{URLField} proposé par Django a une longeur maximale par défaut de \href{https://docs.djangoproject.com/en/3.1/ref/forms/fields/\#django.forms.URLField}{200 caractères}. +\href{https://www.sqlite.org/datatype3.html}{mécanisme de stockage dynamique}, associée à la valeur plutôt qu'au schéma, \emph{via} un système d'affinités. Un autre moteur de base de données définira un schéma statique et rigide, où la valeur sera déterminée par son contenant. +Un champ \texttt{URLField} proposé par Django a une longeur maximale par défaut de \href{https://docs.djangoproject.com/en/3.1/ref/forms/fields/\#django.forms.URLField}{200 caractères}. Si vous faites vos développements sous SQLite et que vous rencontrez une URL de plus de 200 caractères, votre développement sera passera parfaitement bien, mais plantera en production (ou en \emph{staging}, si vous faites les choses peu mieux) parce que les données seront tronquées, et que cela ne plaira pas à la base de données. Conserver des environements similaires limite ce genre de désagréments.} \section{Journaux de flux évènementiels} -Une application ne doit jamais se soucier de l'endroit où les évènements qui la concerne seront écrits, mais se doit simplement de les envoyer sur la sortie \texttt{stdout}. -De cette manière, que nous soyons en développement sur le poste d'un développeur avec une sortie console ou sur une machine de production avec un envoi vers une instance \href{https://www.graylog.org/}{Greylog} ou \href{https://sentry.io/welcome/}{Sentry}, le routage des journaux sera réalisé en fonction de sa nécessité et de sa criticité, et non pas parce que le développeur l'a spécifié en dur dans son code. +Une application ne doit jamais se soucier de l'endroit où les évènements qui la concerne seront écrits, mais se doit simplement de les envoyer sur la sortie \texttt{stdout}. +De cette manière, que nous soyons en développement sur le poste d'un développeur avec une sortie console ou sur une machine de production avec un envoi vers une instance \href{https://www.graylog.org/}{Greylog} ou \href{https://sentry.io/welcome/}{Sentry}, le routage des journaux sera réalisé en fonction de sa nécessité et de sa criticité, et non pas parce que le développeur l'a spécifié en dur dans son code. Cette phase est critique, dans la mesure où les journaux d'exécution sont la seule manière pour une application de communiquer son état vers l'extérieur: recevoir une erreur interne de serveur est une chose; pouvoir obtenir un minimum d'informations, voire un contexte de plantage complet en est une autre. La différence entre ces deux points vous fera, au mieux, gagner plusieurs heures sur l'identification ou la résolution d'un problème. \section{Isolation des tâches administratives} -Evitez qu'une migration ne puisse être démarrée depuis une URL de l'application, ou qu'un envoi massif de notifications ne soit accessible pour n'importe quel utilisateur: les tâches administratives ne doivent être accessibles qu'à un administrateur. +Evitez qu'une migration ne puisse être démarrée depuis une URL de l'application, ou qu'un envoi massif de notifications ne soit accessible pour n'importe quel utilisateur: les tâches administratives ne doivent être accessibles qu'à un administrateur. Les applications 12facteurs favorisent les langages qui mettent un environnement REPL (pour \emph{Read}, \emph{Eval}, \emph{Print} et \emph{Loop}) \index{REPL} à disposition (au hasard: \href{https://pythonprogramminglanguage.com/repl/}{Python} ou \href{https://kotlinlang.org/}{Kotlin}), ce qui facilite les étapes de maintenance. \section{Conclusions}