Compare commits

...

117 Commits

Author SHA1 Message Date
Fred Pauchet 32517aa7b1 Starting references to Release It by Michael T Nygard 2023-04-14 21:51:29 +02:00
Fred Pauchet ae881a52fe Update notes 2023-02-17 10:46:18 +01:00
Fred Pauchet 17b3d7336a Switch from Svelte to NextJS 2022-12-16 22:46:35 +01:00
Fred Pauchet 8ddd800da6 Describe a simple CI pipeline 2022-12-16 22:46:16 +01:00
Fred Pauchet 71b4c7a7fd Improve Python chapter with introspection 2022-12-06 16:12:53 +01:00
Fred Pauchet a439c57757 Include a part about dictionaries 2022-12-06 16:05:35 +01:00
Fred Pauchet 7aef6eebd7 Siren's call (cats on trees :-)) 2022-12-06 15:59:01 +01:00
Fred Pauchet 0c9a3fc59d Describe a simple tree diagram 2022-10-23 22:03:30 +02:00
Fred Pauchet 450f0cee18 Reorder sections 2022-10-23 11:31:57 +02:00
Fred Pauchet e926ca4373 Change permissions on files 2022-10-23 11:27:38 +02:00
Fred Pauchet c8ec00c7ba Need to fill OData section 2022-10-23 11:27:09 +02:00
Fred Pauchet dc1d8bed30 Fix LaTeX build 2022-10-23 11:26:14 +02:00
Fred Pauchet 14fbb68952 Add links to htmx 2022-09-18 19:09:17 +02:00
Fred Pauchet a5210a8353 Rework sentences in architecture.tex 2022-09-11 21:51:16 +02:00
Fred Pauchet a14e622318 Switch graphics to new environment 2022-09-11 20:11:33 +02:00
Fred Pauchet 52c6916a6b Convert includegraphic to graphic environment 2022-09-11 16:46:49 +02:00
Fred Pauchet a238fd4eed Create a new environment to include strechted graphics 2022-09-11 16:46:30 +02:00
Fred Pauchet 8a839b2d7b Add descriptive links for Git 2022-09-11 12:05:35 +02:00
Fred Pauchet 745291ac99 Quit PDF generation on first encountered error 2022-09-10 17:14:53 +02:00
Fred Pauchet 43590682bc Delete old asciidoc sources 2022-09-10 17:12:16 +02:00
Fred Pauchet 1a97286bb0 Implement color boxes
That can be used for advices, dangers or memorization
2022-09-10 17:11:20 +02:00
Fred Pauchet 564c8d2264 Delete ASCIIDOC docker compilation 2022-09-10 14:28:37 +02:00
Fred Pauchet a64032fd48 Changing files permissions (due to ... ?) 2022-09-10 14:26:47 +02:00
Fred Pauchet db8bafa349 Fix docker file to correctly build image 2022-09-10 14:26:00 +02:00
Fred Pauchet af8b7c41b0 Add link to Conventional Commits
continuous-integration/drone/push Build is passing Details
2022-06-18 15:16:03 +02:00
Fred Pauchet f6821ff222 Resize images on chapter 2
continuous-integration/drone/push Build is passing Details
2022-06-18 14:54:33 +02:00
Fred Pauchet 0578564c37 Resize images to fit page layout
continuous-integration/drone/push Build is passing Details
2022-06-17 21:31:50 +02:00
Fred Pauchet 862d2d01f6 Rework introduction
continuous-integration/drone/push Build is passing Details
2022-06-12 17:09:19 +02:00
Fred Pauchet e7b66702dc Finalize integration of Roads and Bridges
continuous-integration/drone/push Build is passing Details
2022-06-11 19:52:24 +02:00
Fred Pauchet 9847f652a1 Itemize several sentences
continuous-integration/drone/push Build is passing Details
2022-06-09 21:55:16 +02:00
Fred Pauchet e2ea706de3 Complete introduction on Python
continuous-integration/drone/push Build is passing Details
2022-06-09 20:50:33 +02:00
Fred Pauchet 81da4e3069 Add references to Roads and Bridges
continuous-integration/drone/push Build is passing Details
2022-06-09 20:40:42 +02:00
Fred Pauchet de1e298225 About dependencies...
continuous-integration/drone/push Build is passing Details
2022-06-09 18:53:00 +02:00
Fred Pauchet 804ada2bea PEP Review
continuous-integration/drone/push Build is passing Details
2022-06-08 19:52:39 +02:00
Fred Pauchet 0158ba537b API + Models services
continuous-integration/drone/push Build is passing Details
2022-06-05 22:01:19 +02:00
Fred Pauchet a41d71d0a8 Structure Discord GDPR page content
continuous-integration/drone/push Build is running Details
2022-06-05 22:00:27 +02:00
Fred Pauchet 224ed5a0c2 Services...
continuous-integration/drone/push Build is passing Details
2022-06-03 21:10:05 +02:00
Fred Pauchet 09a1cd0506 Continuing Alex Krupp's work review
continuous-integration/drone/push Build is passing Details
2022-06-03 20:50:18 +02:00
Fred Pauchet dde67920d6 Continue GDPR description from Discord analysis
continuous-integration/drone/push Build is passing Details
2022-06-02 22:23:59 +02:00
Fred Pauchet 16f522d5ea Add the construct_change_message description 2022-06-02 22:23:42 +02:00
Fred Pauchet e8547083c5 Copy output file to Nextcloud :p
continuous-integration/drone/push Build is passing Details
2022-06-01 22:17:03 +02:00
Fred Pauchet e1c7f7ce46 Annexes, new chapters and typo fixes
continuous-integration/drone/push Build is passing Details
2022-06-01 22:16:48 +02:00
Fred Pauchet 31b8c1dc7d Add a note on Fly.io
continuous-integration/drone/push Build is passing Details
2022-05-31 21:57:21 +02:00
Fred Pauchet 0c2aeee91e Add appendices
continuous-integration/drone/push Build is passing Details
2022-05-31 20:39:48 +02:00
Fred Pauchet 9e8b04ef5e Corrections de texte, ajouts minimes, ... Et Intégration de Calvin & Hobbes !
continuous-integration/drone/push Build is failing Details
2022-05-18 21:52:38 +02:00
Fred Pauchet 8ad6e92bc5 Fix Heroku notes
continuous-integration/drone/push Build is failing Details
2022-05-13 11:26:09 +02:00
Fred Pauchet f901628255 Working on Debian/Ubuntu
continuous-integration/drone/push Build is failing Details
2022-05-11 21:49:21 +02:00
Fred Pauchet f5ae5cdac4 Work on Debian/Ubuntu deployments methods
continuous-integration/drone/push Build is failing Details
2022-05-11 21:36:42 +02:00
Fred Pauchet ad625a1b40 ... to \ldots
continuous-integration/drone/push Build is failing Details
2022-05-11 20:01:39 +02:00
Fred Pauchet bc7c7f74d4 Télémétrie et composants
continuous-integration/drone/push Build is failing Details
2022-05-11 19:35:11 +02:00
Fred Pauchet 9e0ef60e44 Integration de morceaux du DevOps Handbook
continuous-integration/drone/push Build is failing Details
2022-05-11 18:55:41 +02:00
Gregory Trullemans 4f1afe7678 Reformatage de fichier, de code. Je mets de pour des commentaires à l'intérieur des fichiers.
continuous-integration/drone/push Build is failing Details
2022-05-11 16:01:31 +02:00
Gregory Trullemans a88f1854a0 Code review and rewriting 2022-05-11 15:58:18 +02:00
Fred Pauchet c15e05349d Writing, again and again 2022-05-11 15:43:09 +02:00
Fred Pauchet 2f4718c002 Rephrase peripheral components
continuous-integration/drone/push Build is failing Details
2022-05-05 21:44:18 +02:00
Fred Pauchet 10ec49c302 Add a lot of things :D
continuous-integration/drone/push Build is failing Details
2022-05-04 22:01:55 +02:00
Fred Pauchet 5de564b21f Including templates into LaTeX document
continuous-integration/drone/push Build is failing Details
2022-05-03 22:10:58 +02:00
Fred Pauchet 6f868de04b Ignore minted module temporary files
continuous-integration/drone/push Build is failing Details
2022-05-02 21:55:21 +02:00
Fred Pauchet a23dfec354 Retrieve architecture image from data intensive apps
continuous-integration/drone/push Build is failing Details
2022-05-02 20:10:45 +02:00
Fred Pauchet b39fcf9bf7 Move deployment on part 2
continuous-integration/drone/push Build is failing Details
2022-05-01 20:10:26 +02:00
Fred Pauchet f995792784 Easter notes on administration 2022-05-01 20:10:15 +02:00
Fred Pauchet 39f05bd5c3 Easter notes for context processors
continuous-integration/drone/push Build is failing Details
2022-05-01 19:15:52 +02:00
Fred Pauchet e0b0a862cd Easter notes for authentication 2022-05-01 19:13:19 +02:00
Fred Pauchet 7d81286462 Easter notes on administration ,forms, migrations and models 2022-05-01 19:05:58 +02:00
Fred Pauchet c8f88779ff Complete models.tex based on easter notes
continuous-integration/drone/push Build is failing Details
2022-05-01 10:54:22 +02:00
Fred Pauchet 63f2cf23f4 Fix models.tex
continuous-integration/drone/push Build is failing Details
2022-04-30 21:05:17 +02:00
Fred Pauchet f577872744 Review Easter holidays notes (part 2)
continuous-integration/drone/push Build is failing Details
2022-04-30 20:53:42 +02:00
Fred Pauchet e5012f7066 Review from Easter holidays 2022-04-30 19:49:52 +02:00
Fred Pauchet 50310ea9f0 Finalize switch to latex
continuous-integration/drone/push Build is failing Details
2022-04-30 19:05:06 +02:00
Fred Pauchet 6d7b03e59f Include David Revoy latest picture
continuous-integration/drone/push Build is failing Details
2022-04-27 20:08:46 +02:00
Fred Pauchet bc92b559b3 Migrate SOA
continuous-integration/drone/push Build is failing Details
2022-04-27 19:33:47 +02:00
Fred Pauchet 373a39a22b Integrate Debian, forms, ...
continuous-integration/drone/push Build is failing Details
2022-04-26 22:26:00 +02:00
Fred Pauchet b154a4b302 Add forms
continuous-integration/drone/push Build is failing Details
2022-04-25 20:50:14 +02:00
Fred Pauchet ee76783f86 Working a little bit on migrations, models, etc.
continuous-integration/drone/push Build is failing Details
2022-04-25 19:12:16 +02:00
Fred Pauchet 208ea90e2f Working on environment isolation
continuous-integration/drone/push Build is failing Details
2022-04-24 19:00:11 +02:00
Fred Pauchet 9cd685bddd Write down useful tools
continuous-integration/drone/push Build is failing Details
2022-04-22 19:39:38 +02:00
Fred Pauchet 083069d812 Create external tools
continuous-integration/drone/push Build is failing Details
2022-04-21 21:07:42 +02:00
Fred Pauchet 2fd26218da Start new project
continuous-integration/drone/push Build is failing Details
2022-04-20 20:09:48 +02:00
Fred Pauchet c4a7d94926 Rework structure
continuous-integration/drone/push Build is failing Details
2022-04-16 20:53:08 +02:00
Fred Pauchet 9c051318d4 Just finished SOLID
continuous-integration/drone/push Build is failing Details
2022-04-15 22:25:44 +02:00
Fred Pauchet 33e950334a SRP, Open-Closed & Liskov Substitution
continuous-integration/drone/push Build is failing Details
2022-04-14 20:56:45 +02:00
Fred Pauchet b45386ef48 Fix citations references
continuous-integration/drone/push Build is failing Details
2022-04-13 20:06:18 +02:00
Fred Pauchet 261fa2c7c8 12 factors are on the run!
continuous-integration/drone/push Build is failing Details
2022-04-13 20:00:10 +02:00
Fred Pauchet 02930bd52f Structure 12 factors
continuous-integration/drone/push Build is failing Details
2022-04-12 19:56:25 +02:00
Fred Pauchet 7e6ea730de Conclude first chapter
continuous-integration/drone/push Build is failing Details
2022-04-12 19:48:27 +02:00
Fred Pauchet c4ebdaa48b Fix references & complete Python chapter
continuous-integration/drone/push Build is failing Details
2022-04-12 19:18:05 +02:00
Fred Pauchet 32e8fc9ada Continue switch to latex
continuous-integration/drone/push Build is failing Details
2022-03-28 21:27:00 +02:00
Fred Pauchet 80b6ccb26f Move images to the root folder
continuous-integration/drone/push Build is failing Details
2022-03-24 19:40:48 +01:00
Fred Pauchet bd31267ff4 Switching to LaTeX
continuous-integration/drone/push Build is passing Details
2022-03-24 18:56:29 +01:00
Fred Pauchet e4839e2048 Switching to latex (definitively)
continuous-integration/drone/push Build is passing Details
2022-03-23 22:35:04 +01:00
Fred Pauchet b65a23c66b Add automatic LaTeX conversion
continuous-integration/drone/push Build is passing Details
2022-03-23 09:06:22 +01:00
Fred Pauchet a6290ae866 Add LaTeX source folder 2022-03-23 09:05:33 +01:00
Fred Pauchet ac78de067c Change subsection level of linters 2022-03-23 09:05:05 +01:00
Fred Pauchet 64045b66a2 Add a docker-start sh to run latex 2022-03-23 09:02:11 +01:00
Fred Pauchet 44a819ce01 Convert text to docbook 2022-03-23 09:00:15 +01:00
Fred Pauchet 2d4ef253f5 Sphinx is not needed anymore.
It's not like we switched to AsciiDoctor months ago...
2022-03-15 19:07:15 +01:00
Fred Pauchet e8dda4c60d Talk about squashing migrations :)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-03-15 19:04:43 +01:00
Fred Pauchet 850c0381aa Add the dependency graph within migrations
continuous-integration/drone/push Build is passing Details
2022-03-13 19:24:19 +01:00
Fred Pauchet 3370f5b20c Update SOA, ideas, chapters, ...
continuous-integration/drone/push Build is passing Details
2022-03-10 18:36:01 +01:00
Fred Pauchet acc6831726 Copy-paste from my existing doc on Heroku
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-01-05 16:56:41 +01:00
Fred Pauchet bd9417f65a Continuing... :) 2022-01-05 16:32:45 +01:00
Fred Pauchet ea1f1e8925 Make font-size smaller in citations 2022-01-04 14:46:36 +01:00
Fred Pauchet 9e08136f50 Prepare to add a cover
continuous-integration/drone/push Build is passing Details
2022-01-03 11:11:17 +01:00
Fred Pauchet 920341511d Add legacy applications and rework the introduction 2022-01-03 10:54:01 +01:00
Fred Pauchet 14e0f6c73a Finish (I think) SOLID principles
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-01-02 14:25:40 +01:00
Fred Pauchet 79d30a2902 Add a memo for starting a new docker container 2022-01-02 14:24:44 +01:00
Fred Pauchet 718679964c Integrate notes on tests from the devops handbook
continuous-integration/drone/push Build is failing Details
2021-12-31 23:23:09 +01:00
Fred Pauchet 7167745bb3 Integrate devops handbook notes 2021-12-31 23:20:04 +01:00
Fred Pauchet f26ef70f59 Improve models and create SOA
continuous-integration/drone/push Build is failing Details
2021-12-30 19:02:41 +01:00
Fred Pauchet 43ee80f983 Restart data model, by integrating Clean Code practices 2021-12-15 19:13:53 +01:00
Fred Pauchet c75fd76775 Create new parts for datamodel, SOA and go-live
continuous-integration/drone/push Build is passing Details
2021-12-14 21:51:35 +01:00
Fred Pauchet acb10f07e3 Continue gwift-book
continuous-integration/drone/push Build is passing Details
2021-12-04 11:37:37 +01:00
Fred Pauchet 8b66c2b9e9 Integrate IaaS
continuous-integration/drone/push Build is passing Details
2021-11-21 19:59:04 +01:00
Fred Pauchet 551f0f5dad Add a bibliography and some resources
continuous-integration/drone/push Build is failing Details
2021-10-09 22:14:12 +02:00
Fred Pauchet a78a5c2383 Add a CONTRIBUTION guide
continuous-integration/drone/push Build is failing Details
2021-10-04 21:23:22 +02:00
Fred Pauchet c28d0b4cdb Complete references after reviewing each page
continuous-integration/drone/push Build is failing Details
2021-09-30 20:38:47 +02:00
Fred Pauchet 059d7c1f99 Rework some quotes
continuous-integration/drone/push Build is passing Details
2021-09-26 22:06:20 +02:00
244 changed files with 12259 additions and 6372 deletions

0
.drone.yml Normal file → Executable file
View File

2
.gitignore vendored Normal file → Executable file
View File

@ -7,3 +7,5 @@ build
*.log
*.vscode/
*.asciidoctor/
*.aux
_minted-main/

36
CONTRIBUTING.md Executable file
View File

@ -0,0 +1,36 @@
# Contibuer à Gwift
(Grossièrement traduit et adapté de [ProGit](https://raw.githubusercontent.com/progit/progit2/main/CONTRIBUTING.md))
## Licence
Quand vous ouvrez une *pull request*, vous acceptez d'appliquer la même licence que le projet à votre travail.
Aussi, vous acceptez de céder votre travail sous cette même licence.
Si vos modifications devaient apparaitre dans une version publiée, vous apparaitrez dans la [liste des contributeurs](book/contributors.adoc).
## Signaler un problème
Vérifiez avant tout s'il n'existe pas déjà un problème similaire, avant de créer un nouveau ticket.
Aussi, vérifiez si ce même problème n'a pas déjà été corrigé dans les fichiers sources, mais n'aurait pas encore été pris en compte dans une version ultérieure du fichier PDF.
## Petites corrections
Les errata et clarifications basiques seront acceptés si nous sommes d'accord sur le fait qu'ils améliorent le contenu.
Vous pouvez ouvrir un ticket, de manière à ce que nous discutions de la manière dont il faut adresser le changement.
Si vous n'avez jamais réalisé ceci, le [flow guide](https://guides.github.com/introduction/flow/) peut être utile au début.
## Gros changements
Ouvrez d'abord une discussion, de manière à démarrer.
Une grosse modification tend à être très subjective, et ne vise souvent qu'un petit nombre de lecteurs/utilisateurs.
## Images et schéma
Les images de ce livre sont générées en utilisant [Draw.io](draw.io).
## Traductions
Il n'y en a pas pour le moment 😉.

14
Dockerfile Executable file
View File

@ -0,0 +1,14 @@
FROM miktex/miktex
LABEL Description="Docker container from MiKTeX, Ubuntu 20.04, with Pygments" Version="1.0"
RUN rm /etc/apt/sources.list.d/miktex.list
RUN apt-get update
RUN apt-get install python3-pip -y
RUN pip install pygments
WORKDIR /miktex/work
CMD ["bash"]

8
Makefile Normal file → Executable file
View File

@ -9,4 +9,10 @@ clean:
rm -rf $(BUILDDIR)/*
pdf:
asciidoctor-pdf -r asciidoctor-bibtex -a pdf-themesdir=resources/themes -a pdf-theme=book.yml source/main.adoc -t
asciidoctor-pdf \
-r asciidoctor-bibtex \
-a pdf-themesdir=resources/themes \
-a pdf-theme=book.yml \
-a pdf-fontsdir="resources/fonts" \
source/main.adoc -t \
--trace

29
README.md Normal file → Executable file
View File

@ -2,30 +2,13 @@
[![Build Status](https://drone.grimbox.be/api/badges/fred/gwift-book/status.svg)](https://drone.grimbox.be/fred/gwift-book)
Ce livre peut être compilé avec [AsciiDoctor](https://asciidoctor.org/).
Ce livre peut être compilé avec une image Docker personnalisée, basée sur Miktex.
Pygments est également installé.
Elle peut être compilée avec Docker ou Podman, grâce à l'une des commandes suivantes:
* `docker build -t miktex-pygments .`
## Dépendances
Pour compiler le manuel en PDF:
```bash
$ gem install asciidoctor-pdf --pre
$ gem install asciidoctor-bibtex
$ gem install rouge
```
* `./docker-miktex.sh`
## Conversion en PDF
```bash
asciidoctor -a rouge-style=monokai -a pdf-themesdir=resources/themes -a pdf-theme=gwift main.adoc -t -r asciidoctor-diagram
asciidoctor-pdf -a pdf-themesdir=resources/themes -a pdf-theme=gwift main.adoc -t -r asciidoctor-diagram
```
## Image Docker
L'image Docker utilisée par Drone est [celle-ci](https://github.com/asciidoctor/docker-asciidoctor).
Elle convient tout ce qu'il faut pour compiler correctement le document en PDF (asciidoctor-pdf, asciidoctor-bibtex, ...).
## Erreurs connues
Si `asciidoctor` n'est pas dans le PATH malgré son installation, on peut le trouver grâce à la commande `gem env`, puis à réutiliser (ou à modifier le fichier `.bash_profile` ou `.profile`).

55
annexes/gilded-roses.tex Normal file
View File

@ -0,0 +1,55 @@
\chapter{Gilded Roses}
\url{https://github.com/emilybache/GildedRose-Refactoring-Kata}
\begin{listing}[H]
\begin{minted}[tabsize=4]{python}
# -*- coding: utf-8 -*-
class GildedRose(object):
def __init__(self, items):
self.items = items
def update_quality(self):
for item in self.items:
if item.name != "Aged Brie" and item.name != "Backstage passes to a TAFKAL80ETC concert":
if item.quality > 0:
if item.name != "Sulfuras, Hand of Ragnaros":
item.quality = item.quality - 1
else:
if item.quality < 50:
item.quality = item.quality + 1
if item.name == "Backstage passes to a TAFKAL80ETC concert":
if item.sell_in < 11:
if item.quality < 50:
item.quality = item.quality + 1
if item.sell_in < 6:
if item.quality < 50:
item.quality = item.quality + 1
if item.name != "Sulfuras, Hand of Ragnaros":
item.sell_in = item.sell_in - 1
if item.sell_in < 0:
if item.name != "Aged Brie":
if item.name != "Backstage passes to a TAFKAL80ETC concert":
if item.quality > 0:
if item.name != "Sulfuras, Hand of Ragnaros":
item.quality = item.quality - 1
else:
item.quality = item.quality - item.quality
else:
if item.quality < 50:
item.quality = item.quality + 1
class Item:
def __init__(self, name, sell_in, quality):
self.name = name
self.sell_in = sell_in
self.quality = quality
def __repr__(self):
return "%s, %s, %s" % (self.name, self.sell_in, self.quality)
\end{minted}
\end{listing}

71
annexes/grafana.tex Executable file
View File

@ -0,0 +1,71 @@
\chapter{Monitoring Stack}
InfluxDB ? https://www.influxdata.com/
\section{Visualisation}
\begin{quote}
Grafana allows you to query, visualize, alert on and understand your metrics no matter where they are stored.
Create, explore, and share beautiful dashboards with your team and foster a data driven culture.
\end{quote}
\section{Métriques}
\begin{quote}
Graphite is an enterprise-ready monitoring tool that runs equally well on cheap hardware or Cloud infrastructure.
Teams use Graphite to track the performance of their websites, applications, business services, and networked servers.
It marked the start of a new generation of monitoring tools, making it easier than ever to store, retrieve, share, and visualize time-series data.
Graphite was originally designed and written by Chris Davis at Orbitz in 2006 as side project that ultimately grew to be their foundational monitoring tool.
In 2008, Orbitz allowed Graphite to be released under the open source Apache 2.0 license.
Numerous large companies have deployed Graphite to production where it helps them to monitor their production e-commerce services and plan for growth.
\end{quote}
Graphite does two things:
\begin{enumerate}
\item
Store numeric time-series data
\item
Render graphs of this data on demand
\end{enumerate}
What Graphite does not do is collect data for you, however there are some tools out there that know how to send data to graphite. Even though it often requires a little code, sending data to Graphite is very simple.
Graphite consists of 3 software components:
\begin{enumerate}
\item
\textbf{carbon} - a Twisted daemon that listens for time-series data
\item
\textbf{whisper} - a simple database library for storing time-series data (similar in design to RRD)
\item
\textbf{graphite webapp} - A Django webapp that renders graphs on-demand using Cairo
\end{enumerate}
Feeding in your data is pretty easy, typically most of the effort is in collecting the data to begin with. As you send datapoints to Carbon, they become immediately available for graphing in the webapp. The webapp offers several ways to create and display graphs including a simple URL API for rendering that makes it easy to embed graphs in other webpages.
\section{Logs}
\begin{quote}
Loki brings together logs from all your applications and infrastructure in a single place.
By using the exact same service discovery and label model as Prometheus, Grafana Logs can systematically guarantee your logs have consistent metadata with your metrics, making it easy to move from one to the other.
\end{quote}
Loki est l'équivalent (développé directement par Grafana) de Prometheus.
Il sera donc toujours nécessaire d'accumuler des logs au travers d'exporters.
\begin{quote}
Loki se comporte comme Prometheus : c'est un logiciel que vous allez installer sur votre machine qui sert pour le monitoring de votre infrastructure et le laisser vivre sa vie. Comme son mentor, ou presque, il va falloir lui associer des exporters pour le gaver de données : Promtail.
-- https://www.dadall.info/article698/loki-jouer-avec-ses-logs-dans-grafana
\end{quote}
\section{Traces}
Unlike other tracing tools, Grafana Traces does not index the traces which makes it possible to store orders of magnitude more trace data for the same cost, and removes the need for sampling.
Stores orders of magnitude more trace data for the same cost, and removes the need for sampling.
Reduces the TCO by an order of magnitude and makes the system overall much easier to use.
Grafana Traces is available as a containerized application, and you can run it on any orchestration engine like Kubernetes, Mesos, etc. The various services can be horizontally scaled depending on the workload on the ingest/query path. You can also use cloud native object storage, such as Google Cloud Storage, Amazon S3, or Azure Blob Storage.

5
annexes/snippets.tex Executable file
View File

@ -0,0 +1,5 @@
\chapter{Snippets}
\section{Nettoyage de chaînes de caractères}
Basically, NFC is how you normalize text that's meant to be displayed to a user on the web, and NFKC is how you normalize text that's used to for searching and guaranteeing uniqueness. \cite{django_for_startup_founders}

2
annexes/sonar.tex Executable file
View File

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

316
chapters/administration.tex Executable file
View File

@ -0,0 +1,316 @@
\chapter{Administration}
Cette partie est tellement puissante et performante, qu'elle pourrait laisser penser qu'il est possible de réaliser une application complète rien qu'en configurant l'administration.
C'est faux.
L'administration est une sorte de tour de contrôle évoluée, un \emph{back office} sans transpirer; elle se base sur le modèle de données programmé et construit dynamiquement les formulaires qui lui est associé.
Elle joue avec les clés primaires, étrangères, les champs et types de champs par \href{https://fr.wikipedia.org/wiki/Introspection}{introspection}, et présente tout ce qu'il faut pour avoir du \href{https://fr.wikipedia.org/wiki/CRUD}{CRUD} \index{CRUD} \footnote{\emph{Create-Read-Update-Delete}, c'est-à-dire le fonctionnement par défaut de beaucoup d'applications}, c'est-à-dire tout ce qu'il faut pour ajouter, lister, modifier ou supprimer des informations.
Son problème est qu'elle présente une courbe d'apprentissage asymptotique.
Il est \textbf{très} facile d'arriver rapidement à un bon résultat, au travers d'un périmètre de configuration relativement restreint.
Quoi que vous fassiez, il y a un moment où la courbe de paramétrage sera tellement ardue que vous aurez plus facile à développer ce que vous souhaitez ajouter en utilisant les autres concepts de Django.
Cette interface doit rester dans les mains d'administrateurs ou de gestionnaires, et dans leurs mains à eux uniquement: il n'est pas question de donner des droits aux utilisateurs finaux (même si c'est extrêment tentant durant les premiers tours de roues).
Indépendamment de la manière dont vous allez l'utiliser et la configurer, vous finirez par devoir développer une "vraie" application, destinée aux utilisateurs classiques, et répondant à leurs besoins uniquement.
Une bonne idée consiste à développer l'administration dans un premier temps, en \textbf{gardant en tête qu'il sera nécessaire de développer des concepts spécifiques}.
Dans cet objectif, l'administration est un outil exceptionel, qui permet de valider un modèle, de créer des objets rapidement et de valider les liens qui existent entre eux.
C'est aussi un excellent outil de prototypage et de preuve de concept.
Elle se base sur plusieurs couches que l'on a déjà (ou on va bientôt) aborder (suivant le sens de lecture que vous préférez):
\begin{enumerate}
\item
Le modèle de données
\item
Les validateurs
\item
Les formulaires
\item
Les widgets
\end{enumerate}
\section{Le modèle de données}
Comme expliqué ci-dessus, le modèle de données est constité d'un ensemble de champs typés et de relations.
L'administration permet de décrire les données qui peuvent être modifiées, en y associant un ensemble (basique) de permissions.
Si vous vous rappelez de l'application que nous avions créée dans la première partie, les URLs reprenaient déjà la partie suivante:
\begin{minted}[tabsize=4]{python}
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
[...]
]
\end{minted}
Cette URL signifie que la partie \texttt{admin} est déjà active et accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}.
C'est le seul prérequis pour cette partie.
Chaque application nouvellement créée contient par défaut un fichier \texttt{admin.py}, dans lequel il est possible de déclarer les ensembles de données seront accessibles ou éditables.
Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits:
\begin{minted}[tabsize=4]{python}
# gwift/wish/models.py
from django.db import models
class WishList(models.Model):
name = models.CharField(max_length=255)
class Item(models.Model):
name = models.CharField(max_length=255)
wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE)
\end{minted}
Nous pouvons facilement arriver au résultat suivant, en ajoutant
quelques lignes de configuration dans ce fichier \texttt{admin.py}:
\begin{minted}[tabsize=4]{python}
from django.contrib import admin
from .models import Item, WishList
admin.site.register(Item)
admin.site.register(WishList)
\end{minted}
\begin{itemize}
\item
Nous importons les modèles que nous souhaitons gérer dans l'admin
\item
Et nous les déclarons comme gérables. Cette dernière ligne implique aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui n'activera simplement aucune opération de lecture ou modification.
\end{itemize}
Il nous reste une seule étape à réaliser: créer un nouvel utilisateur.
Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}.
\begin{verbatim}
$ python manage.py createsuperuser
Username (leave blank to use 'fred'): fred
Email address: fred@root.org
Password: ******
Password (again): ******
Superuser created successfully.
\end{verbatim}
\begin{figure}[H]
\centering
\includegraphics{images/django/django-site-admin.png}
\caption{Connexion au site d'administration}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics{images/django/django-site-admin-after-connection.png}
\caption{Administration}
\end{figure}
Ceci nous permet déjà d'ajouter des éléments (Items), des listes de souhaits, de visualiser les actions récentes, voire de gérer les autorisations attribuées aux utilisateurs, comme les membres du staff ou les administrateurs.
\section{Quelques conseils de base}
\begin{enumerate}
\item
Surchargez la méthode \texttt{str(self)} pour chaque classe que vous aurez définie dans le modèle.
Cela permettra de construire une représentation textuelle pour chaque instance de votre classe.
Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l'on manipule.
En plus, cette méthode est également appelée lorsque l'administration historisera une action (et comme cette étape sera inaltérable, autant qu'elle soit fixée dans le début).
\item
La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle on peut accéder pour obtenir les détails d'une instance. Par exemple:
\end{enumerate}
\begin{minted}[tabsize=4]{python}
def get_absolute_url(self):
return reverse('myapp.views.details', args=[self.id])
\end{minted}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Les attributs \texttt{Meta}:
\end{enumerate}
\begin{minted}[tabsize=4]{python}
class Meta:
ordering = ['-field1', 'field2']
verbose_name = 'my class in singular'
verbose_name_plural = 'my class when is in a list!'
\end{minted}
\begin{enumerate}
\item
Le titre:
\begin{itemize}
\item
Soit en modifiant le template de l'administration
\item
Soit en ajoutant l'assignation suivante dans le fichier \texttt{urls.py} : \texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}.
\end{itemize}
\item
Prefetch
\end{enumerate}
\url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64}
\url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614}
En gros, le problème de l'admin est que si on fait des requêtes imbriquées, on va flinguer l'application et le chargement de la page.
La solution consiste à utiliser la propriété \texttt{list\_select\_related} de la classe d'Admin, afin d'appliquer une jointure par défaut et et gagner en performances.
\subsection{admin.ModelAdmin}
La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement dans le fichier \texttt{admin.py} de chaque application contiendra la définition de ce que l'on souhaite faire avec nos données dans l'administration. Cette classe (et sa partie Meta)
\subsection{L'affichage}
Comme l'interface d'administration fonctionne (en trèèèès) gros comme un CRUD auto-généré, on trouve par défaut la possibilité de :
\begin{enumerate}
\item
Créer de nouveaux éléments
\item
Lister les éléments existants
\item
Modifier des éléments existants
\item
Supprimer un élément en particulier.
\end{enumerate}
Les affichages sont donc de deux types: en liste et au détail.
Pour les affichages en liste, le plus simple consiste à jouer sur la propriété \texttt{list\_display}.
Par défaut, la première colonne va accueillir le lien vers le formulaire d'édition.
On peut donc modifier ceci, voire créer de nouveaux liens vers d'autres éléments en construisant des URLs dynamiquement.
Voir aussi comment personnaliser le fil d'Ariane ?
\section{Filtres}
Chaque liste permet de spécifier des filtres spécifiques; ceux-ci peuvent être:
\begin{enumerate}
\item \textbf{Appliqués à la liste} (\texttt{list\_filter})
\item \textbf{Horizontaux} (\texttt{filter\_horizontal})
\item \textbf{Verticaux} (\texttt{filter\_vertical})
\item \textbf{Temporels} (\texttt{date\_hierarchy}
\end{enumerate}
\subsection{Appliqués à la liste}
\subsection{Horizontaux}
\subsection{Verticaux}
\subsection{Temporels}
\section{Permissions}
On l'a dit plus haut, il vaut mieux éviter de proposer un accès à l'administration à vos utilisateurs.
Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression.
Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les méthodes suivantes:
\begin{minted}[tabsize=4]{python}
def has_add_permission(self, request):
return True
def has_delete_permission(self, request):
return True
def has_change_permission(self, request):
return True
\end{minted}
On peut accéder aux informations de l'utilisateur actuellement connecté au travers de l'objet \texttt{request.user}.
\section{Relations}
\subsection{Relations 1-N}
Les relations 1-n sont implémentées au travers de formsets (que l'on a normalement déjà décrits plus haut). L'administration permet de les définir d'une manière extrêmement simple, grâce à quelques propriétés.
L'implémentation consiste tout d'abord à définir le comportement du type d'objet référencé (la relation -N), puis à inclure cette définition au niveau du type d'objet référençant (la relation 1-).
\begin{minted}[tabsize=4]{python}
class WishInline(TabularInline):
model = Wish
class Wishlist(admin.ModelAdmin):
...
inlines = [WishInline]
...
\end{minted}
Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist}) pourra directement gérer des relations multiples vers des souhaits.
\subsection{Autocomplétion}
Parler de l'intégration de select2.
\section{Forms}
\section{Présentation}
Parler ici des \texttt{fieldsets} et montrer comment on peut regrouper des champs dans des groupes, ajouter un peu de JavaScript, ...
\section{Actions sur des sélections}
Les actions permettent de partir d'une liste d'éléments, et autorisent
un utilisateur à appliquer une action sur une sélection d'éléments. Par
défaut, il existe déjà une action de \textbf{suppression}.
Les paramètres d'entrée sont :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
L'instance de classe
\item
La requête entrante
\item
Le queryset correspondant à la sélection.
\end{enumerate}
\begin{minted}[tabsize=4]{python}
def double_quantity(self, request, queryset):
for obj in queryset.all():
obj.field += 1
obj.save()
double_quantity.short_description = "Doubler la quantité des souhaits."
\end{minted}
Et pour informer l'utilisateur de ce qui a été réalisé, on peut aussi
lui passer un petit message:
\begin{minted}[tabsize=4]{python}
if rows_updated = 0:
self.message_user(request, "Aucun élément n'a été impacté.")
else:
self.message_user(request, "{} élément(s) mis à jour".format(rows_updated))
\end{minted}
\section{Documentation}
Nous l'avons dit plus haut, l'administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données.
Pour cela, il suffit de suivre les bonnes pratiques, puis \href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer la documentation à partir des URLs}:

396
chapters/api.tex Executable file
View File

@ -0,0 +1,396 @@
\chapter{Application Programming Interface}
\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs
Django Rest Framework vs Marshmallow
Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis:
\begin{itemize}
\item
Intéressante: ouverture
\item
Primordiale: services
\item
La première chose à réaliser: mobile-first
\item
Le cadet de nos soucis: monolithique (cf. Rework)
\end{itemize}
Voir peut-être aussi
\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/}
Remarque : quatre statuts = le minimum syndical. \cite[p. 297]{restful_web_apis} :
\begin{enumerate}
\item
\textbf{200 (OK)}.
Tout va bien.
Le document qui se trouve dans le corps de la réponse, s'il y en a un, est la représentation d'une ressource.
\item
\textbf{301 (Moved Permanently)}.
Reçu lorsque la ressource n'est plus disponible à cette URI.
\item
\textbf{400 (Bad Request)}.
Indique qu'il y a eu un problème côté client.
Le document qui se trouve dans le corps de la réponse, s'il existe, est un message d'erreur.
Avec un peu de chance, le client a la possibilité d'interpréter ce message d'erreur, afin de corriger le problème.
\item
\textbf{500 (Internal Server Error)}.
Il y a un problème côté serveur. Le document présent dans le corps de la réponse, toujours s'il existe, indique le problème.
Comme celui-ci se situe au niveau du serveur, le client ne pourra rien faire pour le résoudre.
\end{enumerate}
Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service d'affectation.
Quelque chose comme ceci:
\begin{minted}{python}
# models.py
from django.db import models
class People(models.Model):
CIVILITY_CHOICES = (
("M", "Monsieur"),
("Mme", "Madame"),
("Dr", "Docteur"),
("Pr", "Professeur"),
("", "")
)
last_name = models.CharField(max_length=255)
first_name = models.CharField(max_length=255)
civility = models.CharField(
max_length=3,
choices=CIVILITY_CHOICES,
default=""
)
def __str__(self):
return "{}, {}".format(self.last_name, self.first_name)
class Service(models.Model):
label = models.CharField(max_length=255)
def __str__(self):
return self.label
class ContractType(models.Model):
label = models.CharField(max_length=255)
short_label = models.CharField(max_length=50)
def __str__(self):
return self.short_label
class Contract(models.Model):
people = models.ForeignKey(People, on_delete=models.CASCADE)
date_begin = models.DateField()
date_end = models.DateField(blank=True, null=True)
contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE)
service = models.ForeignKey(Service, on_delete=models.CASCADE)
def __str__(self):
if self.date_end is not None:
return "A partir du {}, jusqu'au {}, dans le service {} ({})".format(
self.date_begin,
self.date_end,
self.service,
self.contract_type
)
return "A partir du {}, à durée indéterminée, dans le service {}({})".format(
self.date_begin,
self.service,
self.contract_type
)
\end{minted}
\includegraphics{images/rest/models.png}
\section{Mise en place}
La configuration des points de terminaison de notre API peut être relativement touffue.
Pour cette raison, il convient de s'infliger à suivre une structure qui soit similaire pour chaque point de terminaison \cite[Predictability, Rule \#1]{django_for_startup_founders}.
Il convient de:
\begin{enumerate}
\item
\textbf{Spécifier les permissions}
\item
\textbf{Copier et assainir les éléments communiqués en entrée vers des variables locales}
\item
\textbf{Valider les données d'entrée}
\item
\textbf{Enforce business requirements}
\item
\textbf{Perform busines logic}
\item
\textbf{Retourner une réponse HTTP}
\end{enumerate}
-> Répartir les responsabilités selon les composants ci-dessous
\begin{enumerate}
\item
Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de l'API,
\item
Configurer les vues, càd le comportement de chacun des points de terminaison,
\item
Configurer les points de terminaison eux-mêmes, càd les URLs permettant d'accéder aux ressources.
\item
Et finalement ajouter quelques paramètres au niveau de notre application.
\end{enumerate}
\section{Django Rest Framework}
\subsection{Serialiseurs}
Les sérialiseurs agissent litérallement comme des \texttt{forms}, mais au niveau de l'API.
Ils se basent sur un modèle, définit au niveau de la \texttt{class Meta}, permettent de choisir les champs qui seront sérialisés, définissent différentes méthodes d'accès à des propriétés spécifiques et des méthodes de validation.
Tout comme les forms.
Par exemple, avec Django Rest Framework:
\begin{minted}{python}
# serializers.py
from django.contrib.auth.models import User, Group
from rest_framework import serializers
from .models import People, Contract, Service
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class ServiceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Service
fields = ("name",)
\end{minted}
\subsection{Vues}
\begin{minted}{python}
# views.py
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from rest_framework import permissions
from .models import People, Contract, Service
from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer
class PeopleViewSet(viewsets.ModelViewSet):
queryset = People.objects.all()
serializer_class = PeopleSerializer
permission_class = [permissions.IsAuthenticated]
class ContractViewSet(viewsets.ModelViewSet):
queryset = Contract.objects.all()
serializer_class = ContractSerializer
permission_class = [permissions.IsAuthenticated]
class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all()
serializer_class = ServiceSerializer
permission_class = [permissions.IsAuthenticated]
\end{minted}
\subsection{URLs}
\begin{minted}{python}
# urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from core import views
router = routers.DefaultRouter()
router.register(r"people", views.PeopleViewSet)
router.register(r"contracts", views.ContractViewSet)
router.register(r"services", views.ServiceViewSet)
urlpatterns = [
path("api/v1/", include(router.urls)),
path('admin/', admin.site.urls),
]
\end{minted}
\begin{minted}{python}
# settings.py
INSTALLED_APPS = [
...
"rest_framework",
...
]
...
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS':
'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
\end{minted}
\subsection{Résultat}
En nous rendant sur l'URL \texttt{http://localhost:8000/api/v1}, nous obtiendrons ceci:
\includegraphics{images/rest/api-first-example.png}
\subsection{Modéles et relations}
Plus haut, nous avons utilisé une relation de type \texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité. Pour palier à ceci, il existe {[}plusieurs manières de représenter ces
\url{https://www.django-rest-framework.org/api-guide/relations/}:
\begin{enumerate}
\item Soit \textbf{via} un hyperlien, comme ci-dessus,
\item Soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource.
\end{enumerate}
La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats):
\begin{minted}{js}
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
"http://localhost:8000/api/v1/contracts/1/",
"http://localhost:8000/api/v1/contracts/2/"
]
}
]
}
\end{minted}
à ceci:
\begin{minted}{js}
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
{
"date_begin": "2019-01-01",
"date_end": null,
"service": "http://localhost:8000/api/v1/services/1/"
},
{
"date_begin": "2009-01-01",
"date_end": "2021-01-01",
"service": "http://localhost:8000/api/v1/services/1/"
}
]
}
]
}
\end{minted}
La modification se limite à \textbf{surcharger} la propriété, pour
indiquer qu'elle consiste en une instance d'un des sérialiseurs
existants. Nous passons ainsi de ceci
\begin{minted}{python}
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
\end{minted}
à ceci:
\begin{minted}{python}
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
contract_set = ContractSerializer(many=True, read_only=True)
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
\end{minted}
Nous ne faisons donc bien que redéfinir la propriété \texttt{contract\_set} et indiquons qu'il s'agit à présent d'une instance de \texttt{ContractSerializer}, et qu'il est possible d'en avoir plusieurs. C'est tout.
\subsection{Conclusions}
Django-Rest-Framework est une librarie complète qui ajoute énormément de possibilités.
Cependant \cite{django_for_startup_founders}:
\begin{enumerate}
\item
La documentation est \textbf{réellement} compliquée.
Tout nouveau développeur doit appréhender, comprendre et assimiler cette documentation et tous les concepts sous-jacents.
Ceci inclut notamment le fait que tous les verbes HTTP ont été "traduits" (GET -> retrieve, POST -> create, ...).
Ceci a du sens par rapport à la définition d'une interface REST-compliant, mais ajoute une complexité mentale relativement lourde.
\item
Certains concepts de réutilisation sont tellement compliqués qu'ils prennent plus de temps à mettre en place qu'à écrire une ligne de code Python classique
\item
Les sérialiseurs peuvent rapidement devenir difficiles à lire ou relire, spécifiquement lorsque nous utilisons des \textit{nested serializers} ou lorsque les concepts de désérialisation sont abordés.
\end{enumerate}
\section{Marshmallow}
\textit{Marshmallow} est une alternative plus légère à Django-Rest-Framework, et qui présente une interface plus claire, ainsi qu'une documentation plus concise et facile à comprendre.
Une solution plus facile que les sérializeurs de DRF consistera à
\begin{enumerate}
\item
Gérer la validation de données en utilisant Marshmallow
\item
Sérialiser les données en utilisant du code Python \cite{django_for_startup_founders}.
\end{enumerate}
\section{Ninja}
...
\section{OData}
...
\section{Bonnes pratiques}
\subsection{Authentification}
\subsection{Validation des données}
\subsection{Utilisation d'une API Gateway}
\subsection{Rate limiting}
\subsection{Partage des données nécessaires uniquement}

957
chapters/architecture.tex Executable file
View File

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

233
chapters/authentication.tex Executable file
View File

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

View File

@ -0,0 +1,79 @@
\chapter{Context Processors}
Mise en pratique: un \emph{context processor} sert \emph{grosso-modo} à peupler l'ensemble des données transmises des vues aux templates avec des données communes.
Un context processor est un peu l'équivalent d'un middleware, mais entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
Un \emph{context processor} sert \emph{grosso-modo} à peupler l'ensemble des données transmises des vues aux templates avec des données communes. Un context processor est un peu l'équivalent d'un middleware, mais est situé entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
Nous vous proposons deux exemples ci-dessous:
\begin{enumerate}
\item
Le premier récupère le numéro de version de \texttt{Git}.
Ce n'est sans doute pas le plus adéquat à réaliser en production, puisqu'il suffirait que Git ne soit pas installé, que le code ait été déployé d'une autre manière ou d'un soucis de performances.
\item
Le second qui récupère la liste des catégories et sous-catégories d'une base de données fictives.
Ces deux informations pourraient être affichées pour chaque utilisateur et chaque requête faite vers l'application.
Dans l'immédiat, ce n'est pas trop lourd, mais vu que ces informations ne dépendent pas de l'utilisateur, nous pouvons les glisser dans le cache pour faire gagner énormément de temps.
\end{enumerate}
\begin{minted}{python}
# core/context_processors.py
import subprocess
def git_describe(request) -> str:
return {
"git_describe": subprocess.check_output(
["git", "describe", "--always"]
).strip(),
"git_date": subprocess.check_output(
[
"git",
"show",
"-s",
r"--format=%cd",
r"--date=format:%d-%m-%Y"
]
),
}
\end{minted}
Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe} et \texttt{git\_date} dans tous les contextes de tous les templates de l'application.
\begin{minted}{python}
from product.models import SubCategory, Category
def add_variable_to_context(request):
return {
'subCategories': SubCategory.objects.order_by('id').all(),
'categories': Category.objects.order_by("id").all(),
}
\end{minted}
Il convient ensuite d'ajouter ces \textit{context processors} au niveau des options de configuration des templates:
\begin{minted}{python}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates"),],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"..."
"core.context_processors.git_describe",
"core.context_processors.add_variable_to_context",
"...",
],
},
},
]
\end{minted}
Les context processors sont extrêmement utiles pour injecter des données dans chacune des vues.
Il peut cependant être utile d'utiliser un mécanisme de cache pour gagner du temps de traitement, surtout lorsque des appels en base de données doivent être réalisés: ceci évite que ces appels ne soient réalisés trop souvent, alors qu'ils n'évoluent peut-être pas aussi vite.

View File

@ -0,0 +1,50 @@
\chapter{Intégration continue / Déploiement Continu}
\begin{quote}
\textit{New code should never go live without first being audited for correctness and security}. \cite{django_for_startup_founders}
\end{quote}
Les processus d'intégration continue \footnote{CI - Continuous Integration} et de déploiement continu \footnote{CD - Continuous Deployment} permettent d'automatiser des étapes de vérifications, initialement réalisées par un être humain, par des automates réagissant à des évènements particuliers: typiquement, une nouvelle version du code, un nouveau \textit{commit}, la mise à jour d'un des composants, \ldots
\section{Intégration continue}
L'intégration continue consiste à pousser la pyramide des tests jusqu'à son paroxysme, toujours en laissant un automatate jouer son rôle de bouée de sauvetage.
Chaque pipeline peut être modélisé selon plusieurs étapes, en parallèle ou en séquence, appelant chacune un ensemble de règles, d'outils, et proposant deux états de sortie possibles: soit c'est OK, soit pas.
Au sens booléen des termes.
\textit{A priori}, une sortie négative impliquera une \textit{failure} de l'ensemble du pipeline, bien qu'il soit possible de définir des étapes facultatives (pouvant être exécutées sur certains environnements uniquement), etc.
Un pipeline simpliste, mais déjà complet ressemble à ceci:
\begin{minted}{yaml}
matrix:
PYTHON_VERSION:
- latest
- 3.9
- 3.8
pipeline:
lint:
image: python:${PYTHON_VERSION}
commands:
- pip3 install poetry
- poetry install
- poetry run flake8 src/
- poetry run pylint src/
- poetry run bandit --silent -r src/
- poetry run mypy .
unit_tests:
image: python:${PYTHON_VERSION}
commands:
- pip3 install poetry
- poetry config virtualenvs.create false
- poetry run pytest --cov-report term-missing tests/unit/
\end{minted}
\section{Déploiement continu}
Le déploiement continu est une extension de l'intégration continue, qui est jusqu'au-boutiste à s'occuper lui-même de mettre à disposition une nouvelle version, sous réserve qu'elle ait passé les différentes étapes du pipeline.
C'est ainsi qu'Amazon ou certaines grandes \footnote{ie. \textit{"envahissantes"}} entreprises publient jusqu'à plusieurs miliers de nouvelles fonctionnalités \textbf{par jour}.

562
chapters/debian.tex Executable file
View File

@ -0,0 +1,562 @@
\chapter{Debian/Ubuntu}
\includegraphics{images/logos/debian-ubuntu.png}
Le déploiement sur Debian est présenté dans le cadre d'un déploiment on-premises ou IaaS.
Nous verrons trois types de déploiements:
\begin{enumerate}
\item
\textbf{Manuel}, en exécutant chaque étape une par une.
Ce type de déploiement est intéressant à réaliser au moins une fois, afin de comprendre les différents composants utilisés.
\item
\textbf{Semi-automatisé}, \textit{via} Ansible.
Cette manière se base sur ce qui aura été présenté à la première section, tout en faciliant certains aspects.
Plusieurs concepts devront cependant être abordés au préalable, ce qui pourra paraitre rebutant en première instance.
\item
\textbf{Automatisé}, qui reste un déploiement \textit{on-premise}, mais qui prépare déjà le chemin pour les containeurs.
Ce mode de déploiement-ci garde quelques contraintes, mais présente également quelques avantages.
\end{enumerate}
Par soucis de simplification, quelques raccourcis seront pris:
\begin{itemize}
\item
La base de données sera hébergée sur la même machine que l'application
\item
Il n'y aura pas de \textit{load balancers}
\item
\ldots
\end{itemize}
\begin{tabular}{ll}
Avantages &
\textbf{Une grande accessibilité}: un VPS ne coûte que quelques euros par mois et permet de mettre un projet à disposition de plusieurs utilisateurs \\
\hline
Désavantages & Nécessite du temps et de l'investissement personnel \\
& Nécessite de suivre les évolutions et de prendre une petite journée par an pour appliquer les dernières mises à jour \\
& Nécessite de sécuriser soi-même son infrastructure (clés SSH, Fail2ban, \ldots) \\
& Le montage final ressemble un peu à de l'artisanat (bien qu'Ansible "professionalise" sérieusement le tout) \\
\hline
\end{tabular}
Le choix de ces deux systèmes d'exploitation s'explique par une grande liberté, une bonne compatibilité avec différentes architectures, une bonne stabilité générale et une documentation suffisante:
\begin{itemize}
\item
Debian fonctionne sur un mécanisme de \href{https://www.debian.org/releases/}{canaux}: stable, testing et unstable.
Globalement, en choisissant le premier canal, vous aurez des paquets \textit{très} stables et des procédures de sauts de versions correctement documentées.
\item
Ubuntu fonctionne grâce à des versions \textit{Long-Term Support} \index{LTS}, supportées durant cinq ans et sortant tous les deux ans.
En pratique, en installant une version 22.04, celle-ci sera supportée jusqu'en avril 2027, ce qui vous laissera le temps de planifier le \textit{downtime}.
Entre chacun de ces versions LTS, des versions intermédiaires sont mises à dispositions tous les six mois, mais ne sont supportées que durant 9 mois.
Pour cette raison, leur utilisation sur un serveur est fortement déconseillée.
\end{itemize}
Le processus de configuration à :
\begin{enumerate}
\item
Initialiser le système
\item
Déployer ou mettre les sources à disposition
\item
Démarrer un service implémentant une interface WSGI (\textbf{Web Server Gateway Interface}) \index{WSGI}, qui sera chargé de créer autant de petits lutins travailleurs que nous le désirerons (\textit{Gunicorn})
\item
Démarrer un superviseur, qui se chargera de veiller à la bonne santé de nos petits travailleurs, et en créer de nouveaux s'il le juge nécessaire (\textit{Supervisord})
\item
Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes d'un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l'un des travailleurs, pour exécution (\textit{Nginx, Apache, \ldots}).
\end{enumerate}
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, \ldots
Il existe des dizaines d'hébergements typés VPS (\textbf{Virtual Private Server}).
A vous de choisir celui qui vous convient \footnote{Personnellement, j'ai un petit faible pour Hetzner Cloud}.
\section{Initialisation du serveur}
Nous allons commencer par initialiser le système, configurer les droits utilisateurs, installer une version de l'interpréteur Python et configurer les dépendances principales.
\subsection{Configuration des droits utilisateurs}
La toute première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits.
Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur \textbf{root}, car la moindre faille pourrait avoir des conséquences catastrophiques.
Dans l'ordre, nous devons réaliser les étapes suivantes :
\begin{itemize}
\item
Ajouter un nouveau groupe système, intitulé \texttt{webapps}, qui servira à partager des fichiers entre les différents composants.
L'utilisateur qui fait tourner le proxy inverse sera également ajouté à ce groupe, un peu plus tard.
\item
Ajouter un groupe qui servira à gérer la communications \textit{via sockets}, qui consiste en un ensemble normalisé de fonctions de communication entre processus \footnote{\url{https://fr.wikipedia.org/wiki/Berkeley_sockets}}
\item
Créer un utilisateur application, afin de le conserver isolé du reste du système.
Ceci est un principe de sécurité fondamental: si votre application comprend une faille qui permettrait (par exemple) de supprimer un fichier $\lambda$ dont le chemin serait passé en paramètre passer un chemin de fichier en paramètre.
Le fait d'exécuter notre application sous son propre utilisateur empêche au moins que des fichiers hors de son périmètre ne soit supprimés.
\item
Les applications seront placées dans le répertoire \texttt{/home/gwift}
\item
Octroi des droits de notre utilisateur \texttt{gwift} sur son propre répertoire \texttt{/home/gwift}.
\end{itemize}
Pour résumer, l'ensemble de ces commandes nous donne ceci:
\begin{enumerate}
\item \texttt{groupadd --system webapps}
\item \texttt{groupadd --system gunicorn\_sockets}
\item \texttt{useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift}
\item \texttt{mkdir -p /home/gwift}
\item \texttt{chown gwift:webapps /home/gwift}
\end{enumerate}
\subsection{Dépendances systèmes}
Debian et Ubuntu comprennent nativement une version récente de Python 3.
Mettez vos dépôts à jour et installez la avec les commandes suivantes:
\begin{verbatim}
apt update
apt install python3
\end{verbatim}
Si vous souhaitez utiliser une version ultérieure, il suffit de l'installer \textbf{en parallèle} de la version officiellement supportée par votre distribution.
Ou passer par une installation alternative:
\begin{verbatim}
apt install openssl-devel bzip2-devel libffi-devel
wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz
cd Python-3.8*/
./configure --enable-optimizations
make altinstall
\end{verbatim}
\textbf{Attention !} Le paramètre \texttt{altinstall} est primordial.
Sans lui, vous écraserez l'interpréteur initialement supporté par la distribution, et cela pourrait avoir des effets de bord non souhaités.
\subsection{Base de données}
On l'a déjà vu, Django se base sur un pattern type \href{https://www.martinfowler.com/eaaCatalog/activeRecord.html}{ActiveRecords} pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données relationnelles connus :
\begin{itemize}
\item
SQLite (en natif),
\item
MariaDB (en natif depuis Django 3.0),
\item
PostgreSQL au travers de psycopg2 (en natif aussi),
\item
Microsoft SQLServer grâce aux drivers \href{https://github.com/microsoft/mssql-django}{Microsoft}
\item
Oracle via \href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}.
\end{itemize}
Chaque pilote doit être utilisé précautionneusement !
Chaque version de Django n'est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une version spécifique du pilote.
Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause d'un Oracle 12.1).
Ci-dessous, quelques procédures d'installation pour mettre un serveur à disposition.
Les deux plus simples seront MariaDB et PostgreSQL, que nous couvrirons ci-dessous.
\subsection{PostgreSQL}
Dans le cas de Debian ou Ubuntu, nous exécuterons la commande suivante:
\begin{verbatim}
apt install postgresql postgresql-contrib
\end{verbatim}
Ensuite, nous créerons un utilisateur pour la base de données de notre application.
De la même manière que pour l'utilisateur système, il n'est pas acceptable que la chaine de connexion au moteur de base de données soient associées à un compte administrateur:
\begin{verbatim}
# su - postgres
postgres@gwift:~$ createuser --interactive -P
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
\end{verbatim}
Finalement, nous pouvons effectivemment créer la base de données qui hébergera les données:
\begin{verbatim}
postgres@gwift:~$ createdb --owner gwift_user gwift
postgres@gwift:~$ exit
logout
\end{verbatim}
\subsection{MariaDB}
Idem, installation, configuration, backup, tout ça.
A copier de grimboite, je suis sûr davoir des notes là-dessus.
\subsection{Comparatif PostgreSQL/MariaDB}
\begin{tabular}{lll}
Etape & PostgreSQL & MariaDB \\
\hline
Installation & \texttt{apt install postgresql postgresql-contrib} & \texttt{apt install maria-db} \\
\hline
Création de l'utilisateur & & \\
\hline
Création de la base de données & & \\
\hline
Dump & & \\
\end{tabular}
\section{Préparation de l'environment utilisateur}
\begin{verbatim}
su - gwift
cp /etc/skel/.bashrc .
cp /etc/skel/.bash_profile .
ssh-keygen
mkdir bin
mkdir .venvs
mkdir webapps
python3.6 -m venv .venvs/gwift
source .venvs/gwift/bin/activate
cd /home/gwift/webapps
git clone ...
\end{verbatim}
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes
suivantes :
\begin{verbatim}
# en tant qu'utilisateur 'gwift'
source .venvs/gwift/bin/activate
pip install -U pip
pip install -r requirements/base.txt
pip install gunicorn
cd webapps/gwift
gunicorn config.wsgi:application --bind localhost:3000 --settings
=config.settings_production
\end{verbatim}
\subsection{Configuration de l'application}
\begin{verbatim}
SECRET_KEY=<set your secret key here>
ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/gwift/static
DATABASE=
\end{verbatim}
\begin{itemize}
\item
La variable \texttt{SECRET\_KEY} est notamment utilisée pour le
chiffrement des sessions.
\item
On fait confiance à django\_environ pour traduire la chaîne de
connexion à la base de données.
\end{itemize}
\subsection{Création des répertoires de logs}
\begin{verbatim}
mkdir -p /var/www/gwift/static
\end{verbatim}
\subsection{Socket}
Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}:
\begin{verbatim}
D /var/run/webapps 0775 gwift gunicorn_sockets -
\end{verbatim}
Suivi de la création par systemd :
\begin{verbatim}
systemd-tmpfiles --create
\end{verbatim}
\subsection{Gunicorn}
\begin{verbatim}
#!/bin/bash
# defines settings for gunicorn
NAME="gwift"
DJANGODIR=/home/gwift/webapps/gwift
SOCKFILE=/var/run/webapps/gunicorn_gwift.sock
USER=gwift
GROUP=gunicorn_sockets
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production
DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`"
source /home/gwift/.venvs/gwift/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user $USER \
--bind=unix:$SOCKFILE \
--log-level=debug \
--log-file=-
\end{verbatim}
\section{Composants périphériques}
\subsection{Supervsion, keepalive et autoreload}
Pour la supervision, on passe par Supervisor.
Il existe d'autres superviseurs,
\begin{verbatim}
yum install supervisor -y
\end{verbatim}
On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}:
\begin{verbatim}
[program:gwift]
command=/home/gwift/bin/start_gunicorn.sh
user=gwift
stdout_logfile=/var/log/gwift/gwift.log
autostart=true
autorestart=unexpected
redirect_stdout=true
redirect_stderr=true
\end{verbatim}
Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu'il tourne correctement :
\begin{verbatim}
$ mkdir /var/log/gwift
$ chown gwift:nagios /var/log/gwift
$ systemctl enable supervisord
$ systemctl start supervisord.service
$ systemctl status supervisord.service
supervisord.service - Process Monitoring and Control Daemon
Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled;
vendor preset: disabled)
Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago
Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code
=exited, status=0/SUCCESS)
Main PID: 2310 (supervisord)
CGroup: /system.slice/supervisord.service
- 2310 /usr/bin/python /usr/bin/supervisord -c
/etc/supervisord.conf
- 2313 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2317 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2318 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2321 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2322 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
- 2323 /home/gwift/.venvs/gwift/bin/python3
/home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
ls /var/run/webapps
\end{verbatim}
On peut aussi vérifier que l'application est en train de tourner, à l'aide de la commande \texttt{supervisorctl} :
\begin{verbatim}
supervisorctl status gwift
gwift RUNNING pid 31983, uptime 0:01:00
supervisorctl stop gwift
gwift: stopped
root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift
gwift: started
root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift
gwift: stopped
gwift: started
\end{verbatim}
\subsection{Firewall}
\begin{verbatim}
et 443 (HTTPS).
\end{verbatim}
\begin{verbatim}
firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload
\end{verbatim}
\begin{itemize}
\item
On ouvre le port 80, uniquement pour autoriser une connexion HTTP,
mais qui sera immédiatement redirigée vers HTTPS
\item
Et le port 443 (forcément).
\end{itemize}
\subsection{Unattented-upgrades}
\subsection{Healthchecks.io}
\subsection{Reverse proxy}
\begin{verbatim}
yum install nginx -y
usermod -a -G gunicorn_sockets nginx
\end{verbatim}
On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}:
\begin{verbatim}
yum install nginx -y
usermod -a -G gunicorn_sockets nginx
\end{verbatim}
On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}:
\begin{verbatim}
upstream gwift_app {
server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0;
}
server {
listen 80;
server_name <server_name>;
root /var/www/gwift;
error_log /var/log/nginx/gwift_error.log;
access_log /var/log/nginx/gwift_access.log;
client_max_body_size 4G;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /static/ {
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
client_max_body_size 4G;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /static/ {
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_app;
}
}
}
\end{verbatim}
\begin{itemize}
\item
Ce répertoire sera complété par la commande \texttt{collectstatic} que
l'on verra plus tard. L'objectif est que les fichiers ne demandant
aucune intelligence soit directement servis par Nginx. Cela évite
d'avoir un processus Python (relativement lent) qui doive être
instancié pour servir un simple fichier statique.
\item
Afin d'éviter que Django ne reçoive uniquement des requêtes provenant
de 127.0.0.1
\end{itemize}
\subsubsection{Let's Encrypt}
Certificats externes + communication par socket interne.
\section{Mise à jour}
\begin{verbatim}
u - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
kill -HUP `ps -C gunicorn fch -o pid | head -n 1`
\end{verbatim}
\begin{itemize}
\item
\url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file}
\end{itemize}
\subsection{Logrotate}
\begin{verbatim}
/var/log/gwift/* {
weekly
rotate 3
size 10M
compress
delaycompress
}
\end{verbatim}
Puis on démarre logrotate avec \texttt{logrotate -d /etc/logrotate.d/gwift} pour vérifier que cela fonctionne correctement.
\subsection{Sauvegardes}
Les sauvegardes ont été configurées avec borg : \texttt{yum\ install\ borgbackup}.
C'est l'utilisateur gwift qui s'en occupe.
\begin{verbatim}
mkdir -p /home/gwift/borg-backups/
cd /home/gwift/borg-backups/
borg init gwift.borg -e=none
borg create gwift.borg::{now} ~/bin ~/webapps
\end{verbatim}
Et dans le fichier crontab :
\begin{verbatim}
0 23 * * * /home/gwift/bin/backup.sh
\end{verbatim}
\subsection{Conclusions}
Ce type de déploiement est complexe lors d'une première fois, mais est relativement rapide par la suite.
Comptez une heure ou deux lorsque vous aurez pris l'habitude.
\section{Ansible}
On peut aussi passer par fabric, ansible, chef ou puppet.

View File

@ -0,0 +1,5 @@
\chapter{Processus de mises à dispositions}
\section{Waffle}
\section{\ldots}

18
chapters/deployment-tools.tex Executable file
View File

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

33
chapters/docker-compose.tex Executable file
View File

@ -0,0 +1,33 @@
\chapter{Docker-Compose}
\begin{quote}
Docker [\ldots] helps software applications run inside containers.
(Containers provide a clean, tidy environment for software applications that make them easier to run anywhere).
Docker started as an internal project within dotCloud, a platform-as-a-service, but became so popular that the founders decided to make Docker the main focus of the company.
The Docker project was open sourced in 2013.
Docker has raised \$180M with an estimated valuation of over \$1B.
Their business is based on support, private plans, and services.
Docker's 2014 revenue was less than \$10 million \cite[p. 48]{roads_and_bridges}
\end{quote}
(c/c Ced' - 2020-01-24)
Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter pour django.
D'abords, après avoir installer docker-compose et les dépendances sous debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root pour utiliser docker. Ensuite, j'ai relancé mon pc car juste relancé un shell n'a pas suffit pour que je puisse utiliser docker avec mon compte.
Bon après c'est facile, un petit virtualenv pour cookiecutter, suivi d'une installation du template django.
Et puis j'ai suivi \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
Du coup, ça prend vite de la place: image.png
L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque 1,5 GB d'un coup.
Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien faire ou presque.
La partie ci-dessous a été reprise telle quelle de \href{https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html}{la
documentation de cookie-cutter-django}.
le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt source.

123
chapters/filters.tex Executable file
View File

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

175
chapters/forms.tex Executable file
View File

@ -0,0 +1,175 @@
\chapter{Forms}
Ou comment valider proprement des données entrantes.
\begin{graphic}{images/xkcd-327.png}
\caption{XKCD 327}
\end{graphic}
\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.
\end{quote}
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.
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.
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.
\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.
\end{enumerate}
Ils agissent come une glue entre l'utilisateur et la modélisation de vos structures de données.
\section{Flux de validation}
\textbar{} .Validation \textbar{} .is\_valid \textbar{} .clean\_fields ↓
.clean\_fields\_machin
Tout comme pour le modèle (+ ref), l'idée est simplement de définir plusieurs niveaux de validation.
\section{Gestion du changement}
Dans le package \texttt{django.contrib.admin.utils}, on trouve une petite pépite du nom de \texttt{construct\_change\_message}.
Cette fonction permet de construire un message de changement à partir de n'importe que \texttt{form} ou \texttt{formset}.
Elle prend en paramètre une valeur booléenne supplémentaire qui indique s'il s'agit d'un ajout ou pas.
Le résultat retourne une structure qui indique les champs qui ont été modifiés, les champs qui ont été ajoutés ou supprimés:
\begin{minted}{python}
def construct_change_message(form, formsets, add):
[snip]
change_message = []
if add:
change_message.append({"added": {}})
elif form.changed_data:
change_message.append({"changed": {"fields": changed_field_labels}})
if formsets:
with translation_override(None):
for formset in formsets:
for added_object in formset.new_objects:
change_message.append(
{
"added": {
"name": str(added_object._meta.verbose_name),
"object": str(added_object),
}
}
)
for changed_object, changed_fields in formset.changed_objects:
change_message.append(
{
"changed": {
"name": str(changed_object._meta.verbose_name),
"object": str(changed_object),
"fields": _get_changed_field_labels_from_form(
formset.forms[0], changed_fields
),
}
}
)
for deleted_object in formset.deleted_objects:
change_message.append(
{
"deleted": {
"name": str(deleted_object._meta.verbose_name),
"object": str(deleted_object),
}
}
)
return change_message
\end{minted}
\section{Dépendance avec le modèle}
Un \textbf{form} peut hériter 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}
from django import forms
from wish.models import Wishlist
class WishlistCreateForm(forms.ModelForm):
class Meta:
model = Wishlist
fields = ('name', 'description')
\end{minted}
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{}dont\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ quune\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ lattribut\ \textasciigrave{}fields},
nous pourrons nous assurer de faire évoluer le formulaire en fonction du
modèle sur lequel il se base.
L'intérêt du form est de créer une isolation par rapport aux données provenant de l'extérieur.
\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.
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}.
Sinon, ils peuvent l'être directement au niveau du champ.
\subsection{Squelette par défaut}
\subsubsection{as\_p}
\subsubsection{as\_table}
\subsubsection{Vitor}
\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.
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
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{}}.
\end{itemize}
\url{http://dotmobo.github.io/django-crispy-forms.html}
\section{Conclusions}
Toute donnée entrée par l'utilisateur, quelle qu'elle soit, \textbf{doit} passer par une instance de \texttt{form}: qu'il s'agisse d'un formulaire HTML, d'un fichier CSV, d'un parser XML, \ldots
Dès que des informations "de l'extérieur" font mine d'arriver dans le périmètre de votre application, il convient d'appliquer immédiatement des principes de sécurité reconnus.

430
chapters/gdpr.tex Executable file
View File

@ -0,0 +1,430 @@
\chapter{Politique de protection des données}
\begin{quote}
Le règlement général sur la protection des données (RGPD) responsabilise les organismes publics et privés qui traitent leurs données.
Vous collectez ou traitez des données personnelles ? Adoptez les bons réflexes !
-- \url{https://www.cnil.fr/fr/comprendre-le-rgpd}
\end{quote}
\section{TL;DR}
Aka "Les six bons conseils" de la CNIL:
\begin{enumerate}
\item \textbf{Ne collectez que les données vraiment nécessaires}
\item \textbf{Soyez transparents}
\item \textbf{Pensez au droit des personnes}
\item \textbf{Gardez la maîtrise de vos données}
\item \textbf{Identifiez les risques}
\item \textbf{Sécurisez vos données}
\end{enumerate}
\url{https://www.cnil.fr/sites/default/files/atoms/files/bpi-cnil-rgpd_guide-tpe-pme.pdf}
Le cas de \href{https://discord.com/privacy}{Discord} est éloquent et spécifie:
\begin{enumerate}
\item
Un message de bienvenue (c'est toujours plus sympa), pour dire qu'ils sont nos meilleurs copains, qu'ils ne veulent que notre bien et que leur priorité se trouve au niveau de la sécurité et de l'autonomie de leurs utilisateurs.
\item
Une description de la plateforme et des services qu'elle offre, mais aussi des moyens (déjà) par lesquels ils pourraient récupérer des données personnelles: enquêtes, courriels ou réseaux sociaux.
\item
Les informations collectées, selon trois catégories:
\begin{enumerate}
\item Directement fournies par l'utilisateur
\item Celles qui sont collectées automatiquement
\item Celles qui sont recueillies à partir d'autres sources.
\end{enumerate}
\item
Comment les informations sont utilisées
\item
Comment les informations sont partagées
\item
Quelle est la rétention des données
\item
Comment les informations sont protégées
\item
Comment la vie privée est contrôlée
\item
Les transferts de données internationaux
\item
Les services offerts par les tierces parties
\item
Comment contacter le DPO
\item
Les informations spécifiques à certains utilisateurs (Brésil, UK, EEA)
\item
Les informations spécifiques aux utilisateurs habitant en Californie
\item
La liste des changements
\item
Comment les contacter
\end{enumerate}
La lecture de ces quelques éléments montre que ceux-ci doivent immédiatement faire partie de votre description fonctionnelle et technique.
Ils peuvent être regroupés en quatre catégories bien précises:
\begin{enumerate}
\item La collecte des informations
\item L'utilisation des informations
\item Le partage des informations
\end{enumerate}
\section{Collecte des informations}
Comme indiqué ci-dessus, trois types de collectes différentes sont identifiées:
\begin{enumerate}
\item Directement fournies par l'utilisateur
\item Celles qui sont collectées automatiquement
\item Celles qui sont recueillies à partir d'autres sources.
\end{enumerate}
Chacune des informations collectée doit pouvoir avoir sa raison d'être.
Une fois que cette raison est identifiée, cela peut être considéré comme suffisant.
Mais on ne peut pas demander la date de naissance d'une personne (qui permettrait de l'identifier en croisant les informations avec d'autres plateformes) sans une bonne raison.
Une bonne raison (comme nous le verrons ci-dessous) consiste simplement à autoriser certaines fonctionnalités pouvant nécessité un âge minimal.
\subsection{Informations fournies par l'utilisateur}
Les informations fournies par l'utilisateur sont de plusieurs types également:
\begin{enumerate}
\item Identifiants
\item Contenu créé
\item Informations de paiements
\item Actions effectuées
\item Confirmation d'options facultatives
\end{enumerate}
\subsubsection{Identifiants}
\begin{quote}
\textit{When you create a Discord account, you can come up with a username and password, and provide a way of contacting you (such as an email address and/or phone number).
Youll also need to provide your birthday.
To access certain features or communities, you may need to verify your account or add other information (like a verified phone number) to your account.
You may also have the option to add your name or nicknames.}
\end{quote}
Les informations suivantes sont identifiées:
\begin{tabular}{c|c}
Informations & Raison \\
\hline
Nom d'utilisateur et mot de passe & Connexion à la plateforme \\
\hline
Adresse email ou numéro de téléphone & Un moyen de vous contacter \\
\hline
Une date de naissance & Pour vérifier l'accès à certaines fonctionnalités \\
\end{tabular}
\begin{itemize}
\item
\textbf{Account information}.
When you create a Discord account, you can come up with a username and password, and provide a way of contacting you (such as an email address and/or phone number).
Youll also need to provide your birthday.
To access certain features or communities, you may need to verify your account or add other information (like a verified phone number) to your account.
You may also have the option to add your name or nicknames.
\item
\textbf{Content you create}.
This includes any content that you upload to the service.
For example, you may write messages (including drafts), create custom emojis, or upload and share files through the services.
This also includes your profile information and the information you provide when you create servers.
We generally do not store the contents of video or voice calls or channels.
If we were to change that in the future (for example, to facilitate content moderation), we would disclose that to you in advance.
We also dont store streaming content when you share your screen, but we do retain the thumbnail cover image for the stream for a short period of time.
\item
\textbf{Purchase information}.
If you buy any paid services through Discord, you may need to submit a valid payment method and associated billing information, including your full name and billing address.
Our payment processors, like Stripe and PayPal, receive and process your payment information.
Depending on the processor, we may also receive and store certain billing information, including the last four digits of the credit card number associated with the transaction.
If we decide to process our own payments in the future, we would receive and process this information ourselves.
\item
\textbf{Information from actions you take}.
We collect information about your use of and activities on the services.
This includes the friends you add, the servers or other communities you join, your roles in servers, content moderation decisions you make, and other related actions.
\item
\textbf{Information used to enable optional features}.
Certain features, like contact syncing, may require that you provide additional information (or grant us access to such information) to make them work.
This also includes information about third party integrations you choose to enable and the data you authorize those third party services to share with us.
For example, when you link a music streaming account, we may collect information about that account such as the song you are listening to in order to display that information on your profile or as your status (if you have chosen to do so).
\item
\textbf{Other information you provide directly to us}.
You may have the option to submit additional information as you use Discord.
For example, you may choose to participate in our verified server program, which requires that you provide additional information about yourself or your company.
Or, you may participate in surveys where you can provide feedback on the product, or submit information to our Discord Support teams.
\end{itemize}
\subsection{Informations collectées automatiquement}
We also collect information automatically from you when you use Discord. This includes:
\begin{itemize}
\item
\textbf{Information about your device}.
We collect information about the device you are using to access the services.
This includes information like your IP address, operating system information, browser information, and information about your device settings, such as your microphone and/or camera.
\item
\textbf{Information about your use of the apps or websites}.
We collect log and event information related to how and when you use our services (such as the pages, servers, and channels you visit).
\item
\textbf{Other information that we collect automatically}.
When you take certain actions on other sites, we may receive information about you.
For example, when we advertise for Discord on third party platforms, if you click on the ad, we may receive information about which ad you saw and on which platform.
Similarly, we may also receive certain information when you click on a referral link, such as which website you came from.
\end{itemize}
We may receive information from cookies (small text files placed on your computer or device) and similar technologies. First-party cookies are placed by us (and our third-party service providers) and allow you to use the services and to help us analyze and improve your experience and the services. You can control cookies as described in the “How to control your privacy” section below. The services use the following types of cookies:
\begin{itemize}
\item
\textbf{Strictly Necessary Cookies}: These are required for services to function. If you try to use tools to disable these cookies, parts of the services may not work properly.
\item
\textbf{Functional Cookies}: These help us provide enhanced functionality on the services like remembering language preferences. Disabling these could affect some service functionality.
\item
\textbf{Performance Cookies}: These allow us or our third-party analytics providers to learn how you and others use and engage with the services so we can understand and improve them.
\end{itemize}
\section{Utilisation des informations}
Nous revenons à présent au contenu et aux informations apportées par l'utilisateur.
Un point important est qu'il est précisé que "le contenu est à vous, mais vous accordez une licence d'utilisation à l'entreprise".
Cette licence d'utilisation leur permet de :
\begin{itemize}
\item
Utiliser, copier, stocker, distribuer ou communiquer ce contenu
\item
Publier de manière publique et rendre ce contenu visible à d'autres personnes ou utilisateurs, sous réserve que le propriétaire l'ait rendu visible.
\item
Monitorer, modifier, traduire ou reformater le contenu
\item
Sous-licencier le contenu pour le faire fonctionner sur la plateforme ou avec d'autres fournisseurs.
\end{itemize}
Cette licence est mondiale, non-exclusive, sans aucun frais, transférable et perpétuelle.
C'est également le cas avec un éventuel retour d'idées que nous pourrions leur communiquer, dans la mesure où \textit{by sending us feedback, you grant us a non-exclusive, perpetual, irrevocable, transferable license to use the feedback and ideas generated from the feedback without any restrictions, attribution, or compensation to you}.
Bref, ils sont couverts, et vous ne savez pas exactement tout ce qu'ils pourraient faire avec vos données.
De leur côté, ils proposent également du contenu, mais ils en restent propriétaires :-)
Pour le contenu que les autres personnes pourraient publier ou rendre disponibles, il n'est pas permis de le réutiliser sans leur consentement.
\subsection{Pour remplir leur contrat avec l'utilisateur}
\begin{itemize}
\item
\textbf{To provide you with the services}.
We use your information to provide you with the Discord services.
For example, when you start a video call, we process your images and audio to make that work.
We similarly collect and store the messages you send and display them as you direct.
We also use the information you provide to us to create and manage your account and to facilitate purchases.
When you enable optional features (like connecting your Discord account to other platforms), we use information from those services to power the feature (like displaying what song you are listening to on another service within the Discord app).
\item
\textbf{To meet our commitments to the Discord community}.
We work hard to try to make Discord a safe, positive, and inclusive place.
To do so, we use your information to monitor for and take action against users and content that violate our Terms of Service, Community Guidelines, and other policies.
This includes responding to user reports, detecting fraud and malware, and proactively scanning attachments and other content for illegal or harmful content.
We also use certain information, which may include content reported to us and public posts, to develop and improve systems and models that can be automated to more swiftly detect, categorize, and take action against prohibited content or conduct.
\item
\textbf{To personalize the product}.
We use your information to provide, personalize and improve our services.
This information powers our discovery surfaces (so that you see relevant communities or content first), and it helps us surface Discord features and promotions from us and our partners that may be of interest to you.
As discussed in the “How to control your privacy” section below, you can choose whether to allow us to personalize your Discord experience.
\item
\textbf{To contact you}.
We use your information to contact you in connection with your account, such as to verify or secure it with two-factor authentication.
We may also use your information to contact you about important product or policy changes, to send you information about products you have purchased, or just to let you know about new products or features we think youll like.
You may opt-out of receiving marketing communications.
Where local law requires, we will obtain your consent before sending such communications.
\item
\textbf{To provide customer service}.
We use your information to respond to your questions about our products and services, and to investigate bugs or other issues.
\end{itemize}
\subsection{Dans leur propre intérêt}
\begin{itemize}
\item
\textbf{To protect our services}.
We use information to keep our services secure, to prevent misuse, and to enforce our Terms of Service and other policies against users who violate them.
\item
\textbf{To report on our companys performance}.
We use your information to track the fundamental metrics of our business, to perform financial reporting, to respond to regulatory obligations, and to debug billing issues.
\item
\textbf{To improve our services}.
We use your information to help us understand how users interact with our services, what features or products users may want, or to otherwise understand and improve our services.
This includes information about how you use our services and how servers are structured.
We may also use public posts to better understand, for example, what topics public servers cover and what content is most interesting within those servers.
As discussed in the ”How to control your privacy” section below, you can control whether your data is used for these purposes.
\item
\textbf{To advertise our services on other platforms}.
We are proud of the product we've built and spend money advertising it on other platforms in order to bring more users to Discord.
As part of that, we use certain information to assist in the delivery of our advertising, to measure the effectiveness of advertisements for our own products, and to improve such advertisements in the future.
\end{itemize}
\subsection{Pour tout ce qui touche aux contraintes légales}
We retain and use your information in connection with potential legal claims when necessary and for compliance, regulatory, and auditing purposes. For example, we retain information where we are required by law or if we are compelled to do so by a court order or regulatory body. Also, when you exercise any of your applicable legal rights to access, amend, or delete your personal information, we may request identification and verification documents from you for the purpose of confirming your identity.
\subsection{Avec votre consentement}
We may also collect and use personal information with your consent. You can revoke your consent at any time (mostly through our services directly), though note that you might not be able to use any service or feature that requires collection or use of that personal information.
\subsection{Pour protéger les intérêts vitaux d'un tiers}
We may collect or share personal data if we think someones life is in danger—for example, to help resolve an urgent medical situation.
\section{Arrêt}
Your right to terminate. Youre free to stop using Discords services at any time and for any reason. You can delete your Discord account through the User Settings page in the Discord app. You can also disable your account, which restricts the processing of your personal information as described in our Privacy Policy. Disabling your account does not terminate this agreement.Our right to terminate. Subject to applicable law, we reserve the right to suspend or terminate your account and/or your access to some or all of our services with or without notice, at our discretion, including if:
You breach these terms, our policies, or additional terms that apply to specific products.
Were required to do so to comply with a legal requirement or court order.
We reasonably believe termination is necessary to prevent harm to you, us, other users, or third parties.
Your account has been inactive for more than two years.
However, we will give you advance notice if reasonable to do so or required by applicable law. You can appeal any enforcement action we take under these terms here: https://dis.gd/request.
\section{Partage des informations}
\begin{itemize}
\item
\textbf{When you tell us to}.
When you add your content to the services, you are telling us to share that content with certain communities, people, or in the case of public spaces, with anyone who accesses it.
Who can access that information is determined by who can access a particular community.
Server owners or admins set those permissions, and they control whether a server requires an invite link or is open and accessible to anyone.
And these permissions, like the size of the server, may change over time.
Similarly, if you link your Discord account with a third-party service (like a music-streaming service) or participate in a server that has third-party features like bots enabled, you may be telling us to share certain information with that service, or with other Discord users.
You can control this sharing as described in the "How to control your privacy" section below.
We may also share your information as you otherwise instruct us or provide us your consent to do so.
\item
\textbf{With our vendors}.
We may share information with vendors we hire to carry out specific work for us.
This includes payment processors like Stripe and PayPal that process transactions on our behalf and cloud providers like Google that host our data and our services.
We may also share limited information with advertising platforms to help us reach people that we think will like our product and to measure the performance of our ads shown on those platforms.
We do this to help bring more users to Discord, and provide only the information required to facilitate these services.
This may include information like the fact that you installed our app or registered to use Discord.
\item
\textbf{To comply with the law}.
We may share information in response to a request for information if we believe disclosure is required by law, including meeting national security or law enforcement requirements.
Where allowed and feasible, we will attempt to provide you with prior notice before disclosing your information in response to such a request.
Our Transparency Report has additional information about how we respond to requests from governments and law enforcement entities.
\item
\textbf{In an emergency}.
We may share information if we believe in good faith that it's necessary to prevent serious harm to a person.
\item
\textbf{To enforce our policies and rights}.
We may share information if needed to enforce our Terms of Service, Community Guidelines, or other policies, or to protect the rights, property, and safety of ourselves and others.
\item
\textbf{With our related companies}.
We may share information with our related companies, including parents, affiliates, subsidiaries, and other companies under common control and ownership.
\item
\textbf{Sale, Acquisition, or Transfer of Assets}.
We may share information if Discord is evaluating and/or engaging in a merger, acquisition, reorganization, bankruptcy, or sale, transfer, or change in ownership of Discord or any of its assets.
\item
\textbf{Aggregated or de-identified information}.
We may share information about you that has been aggregated or anonymized such that it cannot reasonably be used to identify you.
For example, we may share aggregated user statistics in order to describe our business to partners or the public.
\end{itemize}
\section{Rétention des données}
We retain personal information for as long as it is needed for the purposes for which we collected it. If your account is inactive for more than two years, we may delete it, and we may delete or anonymize any personal information associated with your account.If you submit an ID for an age verification appeal, we will delete it within sixty days after the age appeal ticket is closed.
\section{Protection des informations}
We take a number of steps to help protect your information.
All information sent within our services is encrypted both in transit and at rest.
For example, we use Transport Layer Security (“TLS”) to encrypt text and images in transit.
We also enforce technical and administrative access controls to limit which of our employees and contractors have access to nonpublic personal information.
You can help maintain the security of your account by configuring two-factor authentication.
\section{Contrôle de la vie privée}
We believe that users should be able to tailor their Discord experience to their preferences, including privacy.
And while local laws may require different things, we believe that our users should have the same basic ability to shape their experience no matter where they are in the world.
Heres how you can control how we process your information and how to request access to your information:
\subsection{Be aware of the Discord spaces you choose to participate in}
You can always choose what communities to participate in and what information you add to our services. You can choose what messages to send or post, who to engage with (e.g., one or more particular users or a potentially unlimited group of users), what information to include in your profile, whether to connect any third party services to your Discord account, and more. For example, if you share content in public spaces, it may be accessed by anyone. Public content may also be subject to fewer restrictions under your local laws.
\subsection{Customize your personal Discord settings}
We offer a number of settings that allow you to tailor your experience within Discord. Some of these relate to specific features: for example, you can choose whether to display your current activity in your status via the Activity Status tab in your User Settings page (this is the gear icon next to your name).
You can also access privacy-specific settings in the Privacy \& Safety section of your User Settings. For example, you can decide which types of direct messages are scanned for explicit content, who can add you as a friend, and more. This is also where you can restrict certain types of processing of your information:
\begin{itemize}
\item
\textbf{Restrict our ability to use your data to improve our products. }
If you turn off the “Use data to improve Discord” setting, we will stop collecting and using certain event and log information to help us understand how users use our services, what features or products they may want, or to otherwise improve our services.
\item
\textbf{Restrict our ability to personalize Discord for you}.
If the “Use data to customize my Discord experience” setting is disabled, we will stop collecting and using certain event and log information to help us offer you relevant recommendations for in-app content and features.
\end{itemize}
You can also disable or delete your account via the “My Account” tab on the settings page.
Disabling your account stops the processing of new data, but allows you to reactivate your account without interruption to you.
Deleting your account permanently deletes identifying information and anonymizes other data.
\subsection{Manage your content and servers}
You may edit or erase specific pieces of information within the services:
\begin{itemize}
\item
You can edit or delete any message you have sent or content you have posted if you still have access to the space where you posted it.
\item
You can edit or delete a Discord server if you have the permissions needed to do so.
\item
You can edit or delete a channel from a Discord server if you have the permissions needed to do so.
\end{itemize}
Public posts may be retained by Discord for use as described elsewhere in this policy.
Also, in limited circumstances, we may have a legal obligation to retain certain information, even if you delete the information or your account.
If you want to see what information we have collected about you, you can request a copy of your data by selecting Request Data in the Privacy \& Safety section of your User Settings.
You should receive your data packet within 30 days.
Data is delivered in common digital formats including CSV, JSON, and any other file format you used when uploading attachments to the services.
\subsection{Cookies}
To control how information is collected and used from cookies on the services, you can take one or more of the following steps.
\begin{itemize}
\item
You can disable and manage some cookies through your browser settings. You will need to manage your settings for each browser you use. You can find instructions for how to manage Cookies in popular browsers such as Internet Explorer, Firefox, Chrome, Safari (iOS), Safari (Mac), and Opera.
\item
To disable analytics cookies, you can use the browser controls discussed above or, for certain providers, you can use their individual opt-out mechanisms, such as Google Analytics Opt-Out.
\item
Your mobile device may also include browser settings to manage the use of cookies. Your device may also have settings to control the collection and use information in your apps.
\item
Third party groups also enable you to limit how cookies are used for advertising purposes by participating third parties. You can learn more at Network Advertising Initiative, the Digital Advertising Alliance, and for users in the EU, the European Interactive Digital Advertising Alliance.
\item
Depending on where you are accessing our services, you may be able to adjust your cookie preferences at any time through a cookies banner or by selecting “Cookie Settings” from the footer or menu on our website.
\end{itemize}
If you disable or remove cookies, some parts of the services may not function properly. Information may be collected to remember your opt-out preferences.Questions or concerns about your privacy? You can email us at privacy@discord.com.
\section{Services tiers}
We allow third party developers to build certain features or other services and offer them within the Discord services.
For example, server administrators can add “bots” created by third party developers that provide features like content moderation and interactive games.
Similarly, you may have access to games or activities built by third parties within the services.
These third-party services need to follow all policies that apply to them (including often our Developer Terms of Service and Developer Policy).
As part of these policies, we require developers to have a privacy policy that makes clear what they do with your information.
Please review these privacy policies, as they describe what bots and apps may do with your information.
We also require that certain popular bots apply for access to certain data. But because these services are operated by third parties, we dont control them or what information they collect.
Its up to you whether to participate in a server that uses bots, and whether to engage with third-party services in general.
\section{Conclusions}
la structure proposée est la suivante:
\begin{enumerate}
\item
\end{enumerate}

402
chapters/gwift.tex Executable file
View File

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

262
chapters/heroku.tex Executable file
View File

@ -0,0 +1,262 @@
\chapter{PaaS - Heroku}
\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A Service} \index{PaaS}, où vous choisissez le \emph{service ou le composant} 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. \footnote{Nous prenons ici Heroku, mais il existe d'autres plateformes similaires, notamment \href{https://www.pythonanywhere.com/details/django_hosting}{PythonAnywhere}.}
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 pour déployer une nouvelle instance.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{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.
Par la suite, il conviendra d'investir un minimum: des bases de données sont disponibles, mais montrent rapidement leurs limites \footnote{\ldots et 10 000 enregistrements sont une limite \textbf{très} rapidement atteinte}, tandis qu'aucun mécanisme de migration n'est prévu.
Ceci signifie qu'après avoir démarré un projet en mode "hobby", il vous sera nécessaire de gérer vous-même la migration de vos données, au travers d'un mode opératoire, dans la mesure où aucun processus automatique n'existe entre instances de deux niveaux différents.
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
https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-
temple-86098.git
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "heroku"]
url = https://git.heroku.com/still-thicket-66406.git
fetch = +refs/heads/*:refs/remotes/heroku/*
\end{verbatim}
\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 :
* 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 main}. \footnote{Puisque nous avons précedemment proposé d'utiliser Poetry, la commande \texttt{export} pourra être utilisée pour générer le fichier \texttt{requirements.txt}, comme exigé par Heroku.}
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}.
\section{Configuration du compte}
+ Récupération des valeurs d'environnement pour les réutiliser ci-dessous.
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}
\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 :
\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}.
\end{enumerate}
Une fois installée, connectez-vous :
\begin{verbatim}
$ heroku login
\end{verbatim}
Et créer votre application :
\begin{verbatim}
$ heroku create
Creating app... done, -> young-temple-86098
https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-
temple-86098.git
\end{verbatim}
\begin{figure}
\centering
\includegraphics{images/deployment/heroku-app-created.png}
\caption{Notre application est à présent configurée!}
\end{figure}
Ajoutons lui une base de données, que nous sauvegarderons à intervalle
régulier :
\begin{verbatim}
$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on -> still-thicket-66406... free
Database has been created and is available
! This database is empty. If upgrading, you can transfer
! 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
\end{verbatim}
(+ voir comment récupérer les backups)
\begin{verbatim}
# Copié/collé de https://cookiecutter- django.readthedocs.io/en/latest/deployment-on-heroku.html
heroku create --buildpack https://github.com/heroku/heroku-buildpack-python
heroku addons:create heroku-redis:hobby-dev
heroku addons:create mailgun:starter
heroku config:set PYTHONHASHSEED=random
heroku config:set WEB_CONCURRENCY=4
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 main
heroku run python manage.py createsuperuser
heroku run python manage.py check --deploy
heroku open
\end{verbatim}
\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 :
\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.
\end{enumerate}
\begin{verbatim}
# Procfile
release: python3 manage.py migrate
web: gunicorn gwift.wsg
\end{verbatim}
\section{Hébergement S3}
\begin{verbatim}
# requirements.txt
django==3.2.8
gunicorn
boto3
django-storages
\end{verbatim}
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 :
\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}).
\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.
\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} :
\begin{minted}{python}
AWS_ACCESS_KEY_ID = os.getenv('ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME')
AWS_DEFAULT_ACL = 'public-read'
AWS_LOCATION = 'static'
AWS_S3_SIGNATURE_VERSION = 's3v4'
AWS_S3_HOST = 's3.%s.scw.cloud' % (AWS_S3_REGION_NAME,)
AWS_S3_ENDPOINT_URL = 'https://%s' % (AWS_S3_HOST, )
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3ManifestStaticStorage'
STATIC_URL = '%s/%s/' % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)
# General optimization for faster delivery
AWS_IS_GZIPPED = True
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
\end{minted}
Cette partie pourrait éventuellement se trouver dans un fichier \texttt{heroku.py}, que vous pourriez placer à côté de votre configuration applicative.
Le paramètre \texttt{--settings=} pourrait alors pointer vers ce nouveau fichier, pour ne pas polluer l'environnement par défaut.
Configurez-les dans la console d'administration d'Heroku:
\includegraphics{images/deployment/heroku-vars-reveal.png}
Lors de la publication, vous devriez à présent avoir la sortie suivante,
qui sera confirmée par le \textbf{bucket}:
\begin{verbatim}
remote: -----> $ python manage.py collectstatic --noinput
remote: 128 static files copied, 156 post-processed
\end{verbatim}
\includegraphics{images/deployment/gwift-cloud-s3.png}
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/})
\end{itemize}

14
chapters/i18n.tex Executable file
View File

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

317
chapters/infrastructure.tex Executable file
View File

@ -0,0 +1,317 @@
\chapter{Infrastructure et composants}
L'infrastructure est destinée à supporter deux types d'applications:
\begin{enumerate}
\item
Les applications consommant principalement des cycles processeur (\textit{Compute-intensive applications}).
\item
Les applications consommant intensivement des données (\textit{Data-intensive applications}).
Une application consommant principalement des données est rarement limitée par la puisse du CPU, mais plus souvent par la quantité et la complexité des structures de données, et la vitesse à laquelle celles-ci doivent être échangées. \cite[p. 3]{data_intensive}
\end{enumerate}
Au niveau des composants destinés à épauler ces applications, nous pouvons distinguer les types suivants:
\begin{enumerate}
\item
\textbf{Les composants fonctionnels}, ceux sans qui l'application fonctionnerait de manière partielle, inadéquate, ou avec des temps de réponse inadapté
\item
\textbf{Les composants périphériques}, c'est-à-dire les composants qui aident à avoir une architecture système résiliente, évolutive et maintenable
\item
\textbf{Les composants de supervision}, qui permettent de s'assurer que tout fonctionne correctement, et qu'aucun incident n'est prévu ou n'a été constaté récemment.
\end{enumerate}
\textit{Designing for production} means thinking about production issues as first-class concerns: \cite[pp. 142-143]{release_it}:
\begin{itemize}
\item \textbf{Operations}: Security, availablity, capacity, status, communication,
\item \textbf{Control plane}: Monitoring, deployment, anomaly detection, new features
\item \textbf{Interconnect}: Routing, load balancing, failover, traffic management
\item \textbf{Instances}: Services, processes, components, instance monitoring
\item \textbf{Foundation}: Hardware, VMs, IP addresses, physical network.
\end{itemize}
\section{Composants fonctionnels}
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/diagrams/infrastructure.drawio.png}}
\caption{Une architecture possible pour un système de données moderne, qui combine plusieurs composants \cite[p. 5]{data_intensive}}
\end{figure}
Une application \textit{data-intensive} est généralement composée des blocs fonctionnels suivants \cite[p. 3]{data_intensive} \footnote{De manière non-exhaustive, mais assez complète quand même.}:
\begin{tabular}{|p{0.2\linewidth}|p{0.55\linewidth}|p{0.15\linewidth}|}
\hline
Type & Utilité & Exemples \\
\hline
Bases de données & Stocker des données, pour pouvoir les retrouver (elles-même ou une autre application) plus tard & PostgreSQL, MySQL, MariaDB, SQLite \\
\hline
Caches & Conserver en mémoire des résultats gourmands, afin de pouvoir les réexploiter plus rapidement, sans avoir à les recalculer & Redis, Memcache \\
\hline
Moteurs d'indexation & Autoriser la recherche de résultats sur base de mots-clés ou en utilisant des filtres particuliers & ElasticSearch, Solr \\
\hline
Traitements asynchrones & Exécution de traitements lourds ou pouvant être démarrés \textit{a posteriori} & Celery \\
\hline
\end{tabular}
\subsection{Bases de données}
\subsection{Mise en cache}
\subsection{Moteurs d'indexation}
\subsection{Tâches asynchrones}
\section{Composants périphériques}
Les composants périphériques gravitent autour du fonctionnement normal de l'application.
Ce sont les "petites mains" qui aident à ce que l'application soit résiliente ou que la charge soit amoindrie ou répartie entre plusieurs instances.
Il s'agit des composants comme le proxy inverse ou les distributeurs de charge: ce sont des composants qui sont fortement recommandés, mais pas obligatoires à au bon fonctionnement de l'application.
\begin{quote}
"Focusing on resilience often means that a firm can handle events that may cause crises for most organizations in a manner that is routine and mundane.
Specific architectural patterns that they implemented included fail fasts (settings aggressive timeouts such that failing components don't make the entire system crawl to a halt), fallbacks (designing each feature to degrade or fall back to a lower quality representation), and feature removal (removing non-critical features when they run slowly from any given page to prevent them from impacting the member experience).
Another astonishing example of the resilience that the netflix team created beyond preserving business continuity during the AWS outage, was that they went over six hours into the AWS outage before declaring a Sev 1 incident, assuming that AWS service would eventually be restored (i.e., "AWS will come back\ldots it usually does, right ?").
Only after six hours into the outage did they activate any business continuity procedures. \cite[p. 282]{devops_handbook}
\end{quote}
Avoir des composants périphériques correctement configurés permet d'anticiper ce type d'erreurs, et donc d'augmenter la résilience de l'application. \index{Résilience}
Certaines plateformes, comme Amazon Web Services \index{AWS}, favorisent la flexibilité et l'eslasticité plutôt que la disponibilité d'une seule machine. \cite[p. 8]{data_design}
C'est donc une bonne idée de faire en sorte que des erreurs d'indisponibilité peuvent arriver \footnote{Netflix a par exemple développer le \href{https://github.com/netflix/chaosmonkey}{Chaos Monkey}, qui s'occupe d'éteindre au hasard des machines virtuels et containers dans un environnement de production, juste pour faire en sorte que les équipes soient drillées à développer toutes sortes de mécanismes de remise en service - }.
\begin{tabular}{|p{0.2\linewidth}|p{0.55\linewidth}|p{0.15\linewidth}|}
\hline
Type & Utilité & Exemples \\
\hline
Firewall & & firewalld, UFW \\
\hline
Reverse Proxy & & Nginx \\
\hline
Load balancers & Distribution de la charge sur plusieurs serveurs, en fonction du nombre d'utilisateurs, du nombre de requêtes à gérer et du temps que prennent chacune d'entre elles & HAProxy \\
\hline
Serveurs d'application & & Gunicorn, Uvicorn \\
\hline
\end{tabular}
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), un port et éventuellement du contenu
\item
Le firewall du serveur (Debian GNU/Linux, CentOS, \dots 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.
Sil'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}
\subsection{Firewall}
\subsection{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}}
\subsection{Répartiteur de charge (\textit{Load balancer})}
Les répartiteurs de charges sont super utiles pour donner du mou à l'infrastructure:
\begin{enumerate}
\item
Maintenance et application de patches,
\item
Répartition des utilisateurs connectés,
\item
\ldots
\end{enumerate}
\subsection{Serveurs d'application (\textit{Workers})}
Processus et threads.
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
Renvoyer une réponse à l'utilisateur.
\end{enumerate}
\section{Composants de supervision}
\begin{tabular}{|p{0.2\linewidth}|p{0.55\linewidth}|p{0.15\linewidth}|}
\hline
Type & Utilité & Exemples \\
\hline
Supervision des processus & & Supervisord \\
\hline
Flux d'évènements & & syslogd, journalctl \\
\hline
Notifications & & Zabbix, Nagios, Munin \\
\hline
Télémétrie & & \\
\hline
\end{tabular}
\subsection{Supervision des processus}
https://circus.readthedocs.io/en/latest/, https://uwsgi-docs.readthedocs.io/en/latest/, statsd
\begin{quote}
When we crash an actor or a process, how does a new one get started ?
You could write a bash script with a while() loop in it.
But what happens when the problem persists across restarts ?
The script basically fork-bombs the server. \cite[p. 109-110]{release_it}
\end{quote}
Actors system use a hierarchical tree of supervisors to manage the restarts.
Whenever an actor terminates, the runtime notifies the supervisor.
The supervisor can then decide to restart the child actor, restart all of its children, or crash itself.
If the supervisor crashes, the runtime will terminate all its children and notify the supervisor's supervisor.
Ultimately you can get while branches of the supervision tree to restart with a clean state.
\subsection{Journaux d'évènements}
La présence de journaux, leur structure et de la définition précise de leurs niveaux est essentielle; ce sont eux qui permettent d'obtenir des informations quant au statut de l'application:
\begin{itemize}
\item Que fait-elle pour le moment ?
\item Qu'a-t-elle fait à un moment en particulier ?
\item Depuis quand n'a-t-elle plus émis de nouveaux évènements ?
\end{itemize}
\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.
--- Dan North, ancien consultant chez ToughtWorks
\end{quote}
Chaque évènement est associé à un niveau; celui-ci indique sa criticité et sa émet un premier tri quant à sa pertinence.
\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, \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, \ldots
\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, différents 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}, qui seront pris en charge par gunicorn \index{Gunicorn}.
La configuration que nous allons utiliser est celle-ci:
\begin{enumerate}
\item
\textbf{Formattage}: à définir - mais la variante suivante est complète, lisible et pratique:
\texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}}
\item
\textbf{Handler}: juste un, qui définit un \texttt{StreamHandler}
\item
\textbf{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:
\begin{minted}{python}
import logging
logger = logging.getLogger(__name__)
logger.debug('helloworld')
\end{minted}
\href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par
exemples}.
\subsection{Télémétrie et exploitation des journaux}
Des erreurs sur un environnement de production arriveront, tôt ou tard, et seront sans doute plus compliquée à corriger qu'un morceau de code dans un coin du code.
L'exploitation des journaux permettra de comprendre, analyser, voire corriger, certains incidents.
Comme nous l'avons vu, en fonction de l'infrastructure choisie, il existe plusieurs types de journaux:
\begin{enumerate}
\item Les journaux applicatifs: ie. le flux d'évènements généré par votre application Django
\item Les journaux du serveur: Nginx, Gunicorn, \ldots
\item Les journaux des autres composants: base de données, service de mise en cache, ...
\end{enumerate}
Une manière de faire consiste à se connecter physiquement ou à distance à la machine pour analyser les logs.
En pratique, c'est impossible: entre les répartiteurs de charge, les différents serveurs, \ldots, il vous sera physiquement impossible de récupérer une information cohérente.
La solution consiste à agréger vos journaux à un seul et même endroit:
\includegraphics{images/infrastructure/mattsegal-logging.png}
CC https://mattsegal.dev/django-monitoring-stack.html
\subsection{Sumologic}
\subsection{Alternatives}
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}.
La récupération de métriques augmente la confiance que l'on peut avoir dans la solution.
L'analyse de ces métriques garantit un juste retour d'informations, sous réserve qu'elles soient exploitées correctement.
La première étape consiste à agréger ces données dans un dépôt centralisé, tandis que la seconde étape exigera une structuration correcte des données envoyées.
La collecte des données doit récupérer des données des couches métiers, applicatives et d'environnement.
Ces données couvrent les évènements, les journaux et les métriques - indépendamment de leur source - le pourcentage d'utilisation du processeur, la mémoire utilisée, les disques systèmes, l'utilisation du réseau, \ldots
\begin{enumerate}
\item
\textbf{Métier}: Le nombre de ventes réalisées, le nombre de nouveaux utilisateurs, les résultats de tests A/B, \ldots
\item
\textbf{Application}: Le délai de réalisation d'une transaction, le temps de réponse par utilisateur, \ldots
\item
\textbf{Infrastructure}: Le trafic du serveur Web, le taux d'occupation du CPU, \ldots
\item
\textbf{Côté client}: Les erreurs applicatives, les transactions côté utilisateur, \ldots
\item
\textbf{Pipeline de déploiement}: Statuts des builds, temps de mise à disposition d'une fonctionnalité, fréquence des déploiements, statuts des environnements, \ldots
\end{enumerate}
Bien utilisés, ces outils permettent de prévenir des incidents de manière empirique.
\begin{quote}
Monitoring is so important that our monitoring systems need to be more available and scalable than the systems being monitored.
-- Adrian Cockcroft \cite[p. 200]{devops_handbook}
\end{quote}
Histoire de schématiser, toute équipe se retrouve à un moment où un autre dans la situation suivante: personne ne veut appuyer sur le gros bouton rouge issu de l'automatisation de la chaîne de production et qui dit "Déploiement".
Et pour cause: une fois que nous aurons trouvé une joyeuse victime qui osera braver le danger, il y aura systématiquement des imprévus, des petits détails qui auront été oubliés sur le côté de la route, et qui feront lâchement planter les environnements.
Et puisque nous n'aurons pas (encore) de télémétrie, le seul moment où nous comprendrons qu'il y a un problème, sera quand un utilisateur viendra se plaindre.
\subsection{Statsd}
https://www.datadoghq.com/blog/statsd/

150
chapters/introduction.tex Executable file
View File

@ -0,0 +1,150 @@
\chapter{Introduction}
\begin{quote}
\textit{The only way to go fast, is to go well}
--- Robert C. Martin
\end{quote}
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
\item Parcourant les différentes étapes du développement qui mèneront jusqu'au déploiement
\item Maintenant correctement la base de données
\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.
Dans un article de blog \footnote{\url{http://blog.codinghorror.com/why-ruby/}}, Jeff Atwood, développeur .Net expérimenté, a décrit sa décision de développer Discourse en utilisant le language Ruby \cite[p. 27]{roads_and_bridges}:
\begin{quote}
\textit{Getting up and running with a Microsoft stack is just plain too hard for a developer in, say, Argentina, or Nepal, or Bulgaria.
Open source operating systems, languages, and tool chaines are the great equializer, the basis for the next great generation of programmers all over the world who are goind to help us change the world.}
\end{quote}
La même réflexion s'applique sans effort à Python et Django: l'organisation \textit{Django Girls} a par exemple formé plus de 2000 femmes dans le monde, réparties dans plus de 49 pays \footnote{https://djangogirls.com}.
Django n'a pas été développé par cette organisation elle-même, mais étant \textit{open source}, ce framework peut être téléchargé, utilisé et étudié gratuitement. \cite[p. 28]{roads_and_bridges}.
Si ces morceaux logiciels n'étaient pas libres et accessibles, ils ne pourraient pas être déconstruits, analysés, étudiés et publiés, ni aider certaines personnes à exercer leur propre métier. \footnote{Les termes \textit{free software} et \textit{open source} sont généralement accolés l'un à l'autre; ils ont cependant une conotation politique distincte: le premier est étroitement associé à l'éthique tandis que le second est plus pragmatique. \textit{Open source is a development methodology; free software is a social movement.}\cite{gnu}}
L'ouverture de ces langages et frameworks en fait également un modèle bien défini de documentation et de flexibilité.
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
\end{itemize}
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.
\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.
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 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 Ubuntu)\index{Debian}\index{Ubuntu}, sur une \emph{Plateform as a Service}\index{Platform as a Service}\index{PaaS}, 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.
\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.
\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}}
Ensuite, ce livre s'adresse autant
\begin{enumerate}
\item
au néophyte qui souhaite se lancer dans le développement Web
\item
à l'artisan qui a besoin d'un aide-mémoire et qui ne se rappelle plus toujours du bon ordre des paramètres,
\item
à l'expert qui souhaiterait avoir un aperçu d'une autre technologie que son domaine privilégié de compétences.
\end{enumerate}
Beaucoup de concepts présentés peuvent être oubliés ou restés inconnus jusqu'au moment où ils seront réellement nécessaires: à 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
\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://semaphoreci.com/blog},
\item \url{https://lecourrierduhacker.com/}
\item ou \url{https://www.djangoproject.com/}.
\end{itemize}
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.
\begin{advicebox}
Les conseils indiquent des éléments que nous utilisons personnellement.
\end{advicebox}
\begin{memorizebox}
Les notes importantes indiquent des éléments à retenir.
\end{memorizebox}
\begin{dangerbox}
Les avertissements indiquent un (potentiel) danger ou des élément pouvant amener des conséquences pas spécialement sympathiques.
\end{dangerbox}
Les morceaux de code source seront présentés de la manière suivante:
\begin{listing}[htbp]
\begin{minted}{Python}
# <folder>/<fichier>.<extension>
def function(param):
"""
"""
callback()
\end{minted}
\end{listing}
Chaque extrait de code reprend:
\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
\end{itemize}
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 Microsoft Windows).
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.

2
chapters/js-framework.tex Executable file
View File

@ -0,0 +1,2 @@
\chapter{Next.js}

35
chapters/khana.tex Executable file
View File

@ -0,0 +1,35 @@
\chapter{Khana}
Khana est une application de suivi d'apprentissage pour des élèves ou étudiants.
Nous voulons pouvoir:
\begin{enumerate}
\item
Lister les élèves
\item
Faire des listes de présence pour les élèves
\item
Pouvoir planifier ses cours
\item
Pouvoir suivre l'apprentissage des élèves, les liens qu'ils ont entre
les éléments à apprendre:
\item
pour écrire une phrase, il faut pouvoir écrire des mots, connaître la
grammaire, et connaître la conjugaison
\item
pour écrire des mots, il faut savoir écrire des lettres
\item
\ldots\hspace{0pt}
\end{enumerate}
Plusieurs professeurs s'occupent d'une même classe; il faut pouvoir écrire des notes, envoyer des messages aux autres professeurs, etc.
Il faut également pouvoir définir des dates de contrôle, voir combien de semaines il reste pour s'assurer d'avoir vu toute la matiètre.
Et pouvoir encoder les points des contrôles.
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-khana.png}
\caption{Khana}
\end{figure}

3
chapters/kubernetes.tex Executable file
View File

@ -0,0 +1,3 @@
\chapter{Kubernetes}
Voir ici https://www.youtube.com/watch?v=NAOsLaB6Lfc ( La vidéo dure 5h\ldots )

27
chapters/licence.tex Executable file
View File

@ -0,0 +1,27 @@
\chapter{Licence}
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 :
\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
\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.
\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}.

337
chapters/maintenability.tex Executable file
View File

@ -0,0 +1,337 @@
\chapter{Fiabilité, évolutivité et maintenabilité}
\begin{quote}
The primary cost of maintenance is in spelunking and risk\cite[p. 139]{clean_architecture}
\end{quote}
Que l'utilisateur soit humain, bot automatique ou client Web, la finalité d'un développement est de fournir des applications résilientes, pouvant être mises à l'échelle et maintenables \cite[p. 6]{data_intensive} :
\begin{enumerate}
\item
\textbf{La résilience} consiste à ce que l'application continue à fonctionner \textit{correctement}, c'est-à-dire à ce qu'elle fournisse un service correct au niveau de performance désiré, même quand les choses se passent mal.
Ceci signifie qu'un système a la capacité:
\begin{enumerate}
\item
D'anticiper certains types d'erreurs (ou en tout cas, de les gérer de manière propre), par exemple en proposant une solution de repli (\textit{fallback}) \footnote{Lors d'un incident AWS, Netlix proposait des recommandations statiques et ne se basait plus sur des recommandations personnalisées. Ceci leur a permis de tenir six heures avant de décréter être impacté. \cite{devops_handbook}}
\item
De se rendre rapidement indisponibles les composants qui posent problème, de manière à ce que ceux-ci n'entraînent pas tout le système avec eux (\textit{fail fast})
\item
De retirer certaines fonctionnalités non-critiques lorsqu'elles répondent plus lentement ou lorsqu'elles présentent un impact sur le reste de l'application (\textit{feature removal}).
\end{enumerate}
\item
\textbf{La mise à échelle} consiste à autoriser le système à \textit{grandir} - soit par le trafic pouvant être pris en charge, soit par son volume de données, soit par sa complexité.
\item
\textbf{La maintenabilité} consiste à faire en sorte que toute intervention puisse être réalisée de manière productive: au fil du temps, il est probable que plusieurs personnes se succèdent à travailler sur l'évolution d'une application, qu'ils travaillent sur sa conception ou sur son exploitation.
\end{enumerate}
Une manière de développer de telles applications consiste à suivre la méthodologie des \textbf{12 facteurs} \footnote{\url{https://12factor.net/fr/}}.
Il s'agit d'une méthodologie consistant à suivre douze principes, qui permettent 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'intégration continue \footnote{\url{https://en.wikipedia.org/wiki/Continuous_integration}} ou déploiement continu \footnote{\url{https://en.wikipedia.org/wiki/Continuous_deployment}}.
\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 et à la maintenance de n'importe quel environnement - qu'il soit sur la machine du petit nouveau dans l'équipe, sur un serveur Azure/Heroku/Digital Ocean ou sur votre nouveau Raspberry Pi Zéro planqué à la cave.
Pour reprendre plus spécifiquement les différentes idées derrière cette méthode, nous trouvons des conseils concernant:
\begin{enumerate}
\item
Une base de code unique, suivie par un contrôle de versions
\item
Une déclaration explicite et une isolation des dépendances
\item
Configuration applicative
\item
Ressources externes
\item
La séparation des phases de constructions
\item
La mémoire des processus d'exécution
\item
La liaison des ports
\item
Une connaissance et une confiance dans les processus systèmes
\item
La possibilité d'arrêter élégamment l'application, tout en réduisant au minimum son temps de démarrage
\item
La similarité des environnements
\item
La gestion des journaux et des flux d'évènements
\item
L'isolation des tâches administratives
\end{enumerate}
\section{Une base de code unique, suivie par un contrôle de versions}
Chaque déploiement de l'application, et quel que soit son environnement cible, se basera sur une source unique, afin de minimiser les différences que l'on pourrait trouver entre deux déploiements d'un même projet.
\begin{graphic}{images/12factors/unique-codebase-deploys.png}
\caption{Une base de code unique pour tous les déploiements}
\end{graphic}
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}
En résumé:
\begin{quote}
\textit{Il y a seulement une base de code par application, mais il y aura plusieurs déploiements de lapplication. Un déploiement est une instance en fonctionnement de lapplication. Cest, par exemple, le site en production, ou bien un ou plusieurs sites de validation. En plus de cela, chaque développeur a une copie de lapplication qui fonctionne dans son environnement local de développement, ce qui compte également comme un déploiement}. \footnote{\url{https://12factor.net/fr/codebase}}
\end{quote}
Comme l'explique Eran Messeri, ingénieur dans le groupe Google Developer Infrastructure: "\textit{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 dépôt n'est pas uniquement destiné à hébergé le code source, mais également à d'autres artefacts et autres formes de connaissance, comme les standards de configuration (Chef recipes, Puppet manifests, \ldots), outils de déploiement, standards de tests (y compris ce qui touche à la sécurité), outils d'analyse et de monitoring ou tutoriaux.
\section{Déclaration explicite et isolation des dépendances}
Chaque installation ou configuration doit être toujours réalisée de la même manière, et doit pouvoir être répétée quel que soit l'environnement cible.
Ceci permet d'éviter que l'application n'utilise une dépendance qui ne soit installée que sur l'un des sytèmes de développement, et qu'elle soit difficile, voire impossible, à réinstaller sur un autre environnement.
\begin{memorizebox}
Chaque dépendance devra être déclarée dans un fichier présent au niveau de la base de code.
Lors de la création d'un nouvel environnement vierge, il suffira d'utiliser ce fichier comme paramètre afin d'installer les prérequis au bon fonctionnement de notre application, afin d'assurer une reproductibilité quasi parfaite de l'environnement d'exécution.
\end{memorizebox}
Il est important de bien "épingler" la version liée à chacune des dépendances de l'application.
Ceci peut éviter des effets de bord comme une nouvelle version d'une librairie dans laquelle un bug aurait pu avoir été introduit.\footnote{Le paquet PyLint dépend par exemple d'Astroid; \href{https://github.com/PyCQA/pylint-django/issues/343}{en janvier 2022}, ce dernier a été mis à jour sans respecter le principe de versions sémantiques et introduisant une régression. PyLint spécifiait que sa dépendance avec Astroid devait respecter une version ~2.9. Lors de sa mise à jour en 2.9.1, Astroid a introduit un changement majeur, qui faisait planter Pylint. L'épinglage explicite aurait pu éviter ceci.}.
Dans le cas de Python, la déclaration explicite et l'épinglage pourront notamment être réalisés au travers de \href{https://pypi.org/project/pip/}{PIP} ou \href{https://python-poetry.org/}{Poetry}.
La majorité des langages modernes proposent des mécanismes similaires :
\begin{itemize}
\item \href{https://rubygems.org/}{Gem} pour Ruby,
\item \href{https://www.npmjs.com/}{NPM} pour NodeJS,
\item \href{Maven}{https://maven.apache.org/} pour Java,
\item \ldots
\end{itemize}
\section{Configuration applicative}
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 \ldots
\end{enumerate}
En pratique, toute information susceptible d'évoluer ou de changer (un seuil, une ressource externe, un couple utilisateur/mot de passe, \ldots) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable.
En pratique, avec du code Python/Django, nous pourrions utiliser la libraririe \href{django-environ}{https://pypi.org/project/django-environ/}, qui permet de gérer facilement ce fichier de configuration :
\begin{listing}[H]
\begin{minted}[tabsize=4]{python}
import environ
import os
env = environ.Env(
DEBUG=(bool, False)
)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
DEBUG = env('DEBUG')
SECRET_KEY = env('SECRET_KEY')
DATABASES = {
# Parse database connection url strings
# like psql://user:pass@127.0.0.1:8458/db
'default': env.db(),
}
CACHES = {
'default': env.cache(),
}
\end{minted}
\end{listing}
Il suffira ensuite d'appeler ces variables grâce à \texttt{from django.conf import settings} pour récupérer la valeur qui aura été configurée.
Ceci permet de paramétrer facilement n'importe quel environnement, simplement en modifiant une variable de ce fichier de configuration.
Toute clé de configuration, qu'il s'agisse d'une chaîne de connexion vers une base de données, l'adresse d'un service Web externe, une clé d'API pour l'interrogation d'une ressource, d'un chemin vers un fichier à interprêter ou à transférer, \ldots, ou n'importe quelle valeur susceptible d'évoluer, ne doit se trouver écrite en dur dans le code.
\begin{dangerbox}
Il est important de ne surtout pas ajouter ce fichier \texttt{.env} dans le dépôt de sources: à aucun moment, nous ne devons y trouver de mot de passe ou d'information confidentielle en clair. \footnote{Ainsi, nous pourrions faire une \href{https://github.com/search?q=filenamefilename:.env DB_USERNAME&type=Code}{recherche sur Github} pour retrouver certaines variables d'environnement qui auraient été laissées en dur dans le code source de certains projets. Le \href{https://github.com/techgaun/github-dorks}{dépôt suivant} liste quelques idées de variables à rechercher\ldots}.
\end{dangerbox}
Au moment de développer une nouvelle fonctionnalité, réfléchissez si l'un des paramètres utilisés risquerait de subir une modification ou s'il concerne un principe de sécurité.
Le risque de se retrouver avec une liste colossale de paramètres n'est cependant pas négligeable; pensez à leur assigner une variable par défaut \footnote{Par exemple, Gitea expose \href{la liste suivante de paramètres disponibles}{https://docs.gitea.io/en-us/config-cheat-sheet/}; il serait impossible d'utiliser cette plateforme si chacun d'entre eux devait obligatoirement être configuré avant de pouvoir démarrer une instance.}
\section{Ressources externes}
Nous parlons de bases de données, de services de mise en cache, d'API externes, \ldots L'application doit être capable d'effectuer des changements au niveau de ces ressources sans que son code ne soit modifié.
Nous parlons alors de \textbf{ressources attachées}, dont la présence est nécessaire au bon fonctionnement de l'application, mais pour lesquelles le \textbf{type} n'est pas obligatoirement défini.
Nous voulons par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached", afin que les ressources externes puissent être attachées ou détachées en fonction de leur nécessité, et sans avoir à appliquer une modification au niveau du code applicatif.
Si une base de données ne fonctionne pas correctement, par exemple suite à un problème matériel, l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que son code source ne soit modifié.
Ces ressources sont donc spécifiés grâce à des variables d'environnement, et chacune d'entre elles dispose également d'un \textbf{type}, afin de profiter d'une correspondance dynamique entre un moteur d'exécution et une information de configuration.
\begin{graphic}{images/12factors/attached-resources.png}
\caption{Gestion des ressources attachées}
\end{graphic}
Nous serons ainsi ravis de pouvoir simplement modifier la chaîne de connexion \texttt{sqlite:////tmp/my-tmp-sqlite.db} en \texttt{psql://user:pass@127.0.0.1:8458/db} lorsque ce sera nécessaire, sans avoir à recompiler ou redéployer les modifications.
\section{Séparation des phases de construction}
\begin{enumerate}
\item
La \textbf{construction} (\emph{build}) convertit un code source en un ensemble de fichiers exécutables, associé à une version et à une transaction dans le système de gestion de sources.
\item
La \textbf{mise à disposition} (\emph{release}) associe cet ensemble à une configuration prête à être exécutée sur un environnement désigné,
\item
La phase d'\textbf{exécution} (\emph{run}) démarre les processus nécessaires au bon fonctionnement de l'application.
\end{enumerate}
\begin{graphic}{images/12factors/separate-run-steps.png}
\caption{Séparation des phases de construction, mise à disposition et exécution}
\end{graphic}
Parmi les solutions possibles, nous pourrions nous pourrions nous baser
sur les \emph{releases} de Gitea, sur un serveur d'artefacts (\href{https://fr.wikipedia.org/wiki/Capistrano_(logiciel)}{Capistrano}), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, \ldots).
\begin{memorizebox}
Dans le cas de Python, la phase de construction (\textit{Build}) correspondra plutôt à une phase d'\textit{empaquettage} (\textit{packaging}).
Une fois préparée, la librairie ou l'application pourra être publiée sur \url{pypi.org} ou un dépôt géré en interne.
La phase de \emph{release} revient à associer une version spécifiquement empaquêtée pour l'associer à une configuration particulière.
\end{memorizebox}
\section{Mémoire des processus d'exécution}
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é.
\begin{memorizebox}
L'exécution de l'application ne doit pas dépendre de la présence d'une information locale à son environnement d'exécution.
\end{memorizebox}
En pratique, si l'application devait rencontrer un problème - par exemple suite à un problème matériel, une coupure réseau, \ldots -, il est nécessaire qu'elle puisse redémarrer rapidement, éventuellement en étant déployée sur un autre serveur.
Toute information stockée physiquement sera alors perdue, puisque le contexte d'exécution aura été déplacée à un autre endroit.
Lors de l'initialisation ou de la réinitialisation d'une application, la solution consiste à 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), afin de faire en sorte que les informations et données primordiales puissent être récupérées ou reconstruites, et donc réutilisées, sans altérer le comportement attendu.
\begin{graphic}{images/12factors/execution-process-memory.drawio.png}
\caption{La mémoire des processus d'exécution doit rester accessible, même après leur disparition}
\end{graphic}
Ceci joue énormément sur les possibilités de mise à l'échelle: lors de l'ajout d'un nouveau serveur, il est indispensable qu'il puisse accéder à l'ensemble des informations nécessaires au bon fonctionnement de l'application.
\section{Liaison des ports}
Les applications 12-factors sont auto-contenues et peuvent fonctionner en autonomie totale.
Elles doivent être joignables grâce à un mécanisme de ponts, où l'hôte qui s'occupe de l'exécution effectue lui-même la redirection vers l'un des ports ouverts par l'application, typiquement, en HTTP ou via un autre protocole.
\begin{graphic}{images/diagrams/12-factors-7.png}
\caption{Liaison des ports entre le monde extérieur et ce qu'offre l'application}
\end{graphic}
L'application fonctionne de manière autonome et expose un port (ici, le 8000).
Le serveur (= l'hôte) choisit d'appliquer une correspondance entre "son" port 443 et le port offert par l'application (8000).
\section{Connaissance et confiance des processus systèmes}
Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus sans état (\textit{stateless}), c'est-à-dire qu'aucune information ne doit être stockée localement ou ne doit dépendre d'une information précédemment acquise et accessible uniquement à un processus en particulier.
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:
\begin{itemize}
\item Requêtes HTTP \emph{via} des processus Web;
\item Jobs asynchrones pour des tâches à effectuer en arrière-plan,
\item \ldots
\end{itemize}
Si l'un des concepts dont vous avez besoin existe déjà et peut être utilisé, ne vous fatiguez pas: utilisez le et ne réinventez pas surtout la roue.
\begin{graphic}{images/12factors/process-types.png}
\caption{Charge et types de processus}
\end{graphic}
\section{Arrêts élégants et démarrages rapides}
Par "arrêt élégant", nous voulons surtout éviter le fameux \texttt{kill -9 <pid>} (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:
\begin{enumerate}
\item Les requêtes en cours peuvent se terminer sans impact majeur,
\item Le démarrage (rapide) de nouveaux processus améliore la balance de charge 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}
\begin{graphic}{images/12factors/process-type-chronology.png}
\caption{Evolution des types de processus, en fonction des arrêts et démarrages}
\end{graphic}
L'intégration de ces mécanismes dès les premières étapes de développement limitera les perturbations et facilitera la prise en compte d'arrêts inopinés (problème matériel, redémarrage du système hôte, etc.).
\section{Similarité des environnements}
\begin{dangerbox}
Conservez les différents environnements aussi similaires que possible, et limitez les divergences entre un environnement de développement et de production.
\end{dangerbox}
L'exemple donné est un développeur qui utilise macOS, NGinx et SQLite pour l'élaboration d'une application, tandis que l'environnement de production tourne sur une machine Debian 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}
\begin{memorizebox}
Pour donner un exemple tout bête, SQLite utilise un mécanisme de stockage dynamique \footnote{\url{https://www.sqlite.org/datatype3.html}}, 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 200 caractères \footnote{\url{https://docs.djangoproject.com/en/3.1/ref/forms/fields/\#django.forms.URLField}}.
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 un 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.
\end{memorizebox}
Ceci permet également de proposer à nos utilisateurs un bac à sable dans lequel ils pourront explorer et réaliser des expérimentations en toute sécurité, sans quel cela n'ait d'impact sur un \textit{réel} environnement de production, où les conséquences pourraient être beaucoup plus graves. \cite[p. 9]{data_intensive}
\section{Flux d'évènements}
Les journaux d'exécution sont la seule manière pour une application de communiquer son état et de décrire ce qu'elle est occupée à faire ou à réaliser.
Que nous soyons en phase de développement sur le poste d'une personne de l'équipe, 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 évènements doit être réalisé en fonction de la nécessité du contexte d'exécution et de sa criticité.
\begin{memorizebox}
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}.
\end{memorizebox}
Cette phase est essentielle, dans la mesure où 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.
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 Kotlin \footnote{\url{https://kotlinlang.org/}}), ce qui facilite les étapes de maintenance.
\section{Conclusions}
Une application devient nettement plus maintenable dès lors que l'équipe de développement suit de près les différentes étapes de sa conception, de la demande jusqu'à son aboutissement en production. \cite[pp. 293-294]{devops_handbook}.
Au fur et à mesure que le code est délibérément construit pour être maintenable, l'équipe gagne en rapidité, en qualité et en fiabilité de déploiement, ce qui facilite les tâches opérationnelles:
\begin{enumerate}
\item
Activation d'une télémétrie suffisante dans les applications et les environnements
\item
Conservation précise des dépendances nécessaires
\item
Résilience des services et plantage élégant (i.e. sans finir un SEGFAULT avec l'OS dans les choux et un écran bleu)
\item
Compatibilité entre les différentes versions (n+1, \ldots)
\item
Gestion de l'espace de stockage associé à un environnement (pour éviter d'avoir un environnement de production qui fait 157 Tera-octets)
\item
Activation de la recherche dans les logs
\item
Traces des requêtes provenant des utilisateurs, indépendamment des services utilisés
\item
Centralisation de la configuration (\textit{via} ZooKeeper, par exemple)
\end{enumerate}

521
chapters/migrations.tex Executable file
View File

@ -0,0 +1,521 @@
\chapter{Migrations}
Dans cette section, nous allons voir comment fonctionnent les migrations.
Lors d'une première approche, elles peuvent sembler un peu magiques, puisqu'elles centralisent un ensemble de modifications pouvant être répétées sur un schéma de données, en tenant compte de ce qui a déjà été appliqué et en vérifiant quelles migrations devaient encore l'être pour mettre l'application à niveau. Une analyse en profondeur montrera qu'elles ne sont pas plus complexes à suivre et à comprendre qu'un ensemble de fonctions de gestion appliquées à notre application.
L'intégration des migrations a été réalisée dans la version 1.7 de Django.
Avant cela, il convenait de passer par une librairie tierce intitulée \href{https://south.readthedocs.io/en/latest}{South}.
Prenons l'exemple de notre liste de souhaits; nous nous rendons (bêtement) compte que nous avons oublié d'ajouter un champ de \texttt{description} à une liste.
Historiquement, cette action nécessitait l'intervention d'un administrateur système ou d'une personne
ayant accès au schéma de la base de données, à partir duquel ce-dit utilisateur pouvait jouer manuellement un script SQL. \index{SQL}
Cet enchaînement d'étapes nécessitait une bonne coordination d'équipe, mais également une bonne confiance dans les scripts à exécuter.
Et souvenez-vous (cf. ref-à-insérer), que l'ensemble des actions doit être répétable et automatisable.
Bref, dans les années '80, il convenait de jouer ceci après s'être connecté au serveur de base de données:
\begin{minted}{sql}
ALTER TABLE WishList ADD COLUMN Description nvarchar(MAX);
\end{minted}
Et là, nous nous rappelons qu'un client tourne sur Oracle et pas sur MySQL, et qu'il a donc besoin de son propre script d'exécution, parce que le type du nouveau champ n'est pas exactement le même entre deux moteurs différents:
\begin{minted}{sql}
-- Firebird
ALTER TABLE Category ALTER COLUMN Name type varchar(2000)
-- MSSQL
ALTER TABLE Category ALTER Column Name varchar(2000)
-- Oracle
ALTER TABLE Category MODIFY Name varchar2(2000
\end{minted}
En bref, les problèmes suivants apparaissent très rapidement:
\begin{enumerate}
\item
\textbf{Manque d'autonomie}: il est nécessaire d'avoir les compétences d'une
personne tierce pour avancer ou de disposer des droits
administrateurs,
\item
\textbf{Manque d'automatisation possible}, à moins d'écrire un programme, qu'il
faudra également maintenir et intégrer au niveau des tests
\item
\textbf{Nécessité de maintenir des scripts} différents, en fonction des
moteurs de base de données supportés
\item
\textbf{Manque de vérification} si un script a déjà été exécuté ou non,
à moins, à nouveau, de maintenir un programme ou une table supplémentaire.
\end{enumerate}
\section{Fonctionnement général}
Le moteur de migrations résout la plupart de ces soucis: le framework embarque ses propres applications, dont les migrations, qui gèrent elles-mêmes l'arbre de dépendances entre les modifications qui doivent être appliquées.
Pour reprendre un de nos exemples précédents, nous avions créé un modèle contenant deux classes, qui correspondent chacun à une table dans un modèle relationnel:
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
\end{minted}
Nous avions ensuite modifié la clé de liaison, pour permettre d'associer plusieurs catégories à un même livre, et inversément:
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category, on_delete=models.CASCADE)
\end{minted}
Chronologiquement, cela nous a donné
\begin{enumerate}
\item Une première migration consistant à créer le modèle initial
\item Suivie d'une seconde migration après que nous ayons modifié le modèle pour autoriser des relations multiples
\end{enumerate}
\begin{minted}{python}
# library/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="Book",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"title",
models.CharField(max_length=255)
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="library.category",
),
),
],
),
]
\end{minted}
\begin{itemize}
\item
La migration crée un nouveau modèle intitulé "Category", possédant un
champ \texttt{id} (auto-défini, puisque nous n'avons rien fait), ainsi
qu'un champ \texttt{name} de type texte et d'une longue maximale de
255 caractères.
\item
Elle crée un deuxième modèle intitulé "Book", possédant trois champs:
son identifiant auto-généré \texttt{id}, son titre \texttt{title} et
sa relation vers une catégorie, au travers du champ \texttt{category}.
\end{itemize}
Un outil comme \href{https://sqlitebrowser.org/}{DB Browser For SQLite} nous donne la structure suivante:
\includegraphics{images/db/migrations-0001-to-0002.png}
La représentation au niveau de la base de données est la suivante:
\includegraphics{images/db/link-book-category-fk.drawio.png}
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category)
\end{minted}
Vous noterez que l'attribut \texttt{on\_delete} n'est plus nécessaire.
Après cette modification, la migration résultante à appliquer correspondra à ceci. En SQL, un champ de type \texttt{ManyToMany} ne peut qu'être représenté par une table intermédiaire.
Ce qu'applique la migration en supprimant le champ liant initialement un livre à une catégorie et en ajoutant une nouvelle table de liaison.
\begin{minted}{python}
# library/migrations/0002_remove_book_category_book_category.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.Category'),
),
]
\end{minted}
\begin{itemize}
\item
La migration supprime l'ancienne clé étrangère \ldots
\item
\ldots et ajoute une nouvelle table, permettant de lier nos catégories à nos livres.
\end{itemize}
\includegraphics{images/db/migrations-0002-many-to-many.png}
Nous obtenons à présent la représentation suivante en base de données:
\includegraphics{images/db/link-book-category-m2m.drawio.png}
\section{Graph de dépendances}
Lorsqu'une migration applique une modification au schéma d'une base de données, il est évident qu'elle ne peut pas être appliquée dans n'importe quel ordre ou à n'importe quel moment.
Dès la création d'un nouveau projet, avec une configuration par défaut et même sans avoir ajouté d'applications, Django proposera immédiatement d'appliquer les migrations des applications \textbf{admin},
\textbf{auth}, \textbf{contenttypes} et \textbf{sessions}, qui font partie du coeur du système, et qui se trouvent respectivement aux emplacements suivants:
\begin{itemize}
\item
\textbf{admin}: \texttt{site-packages/django/contrib/admin/migrations}
\item
\textbf{auth}: \texttt{site-packages/django/contrib/auth/migrations}
\item
\textbf{contenttypes}:
\texttt{site-packages/django/contrib/contenttypes/migrations}
\item
\textbf{sessions}:
\texttt{site-packages/django/contrib/sessions/migrations}
\end{itemize}
Ceci est dû au fait que, toujours par défaut, ces applications sont reprises au niveau de la configuration d'un nouveau projet, dans le fichier \texttt{settings.py}:
\begin{minted}{python}
[snip]
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
[snip]
\end{minted}
Dès que nous les appliquerons, nous recevrons les messages suivants:
\begin{verbatim}
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, library, sessions, world
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
\end{verbatim}
Cet ordre est défini au niveau de la propriété \texttt{dependencies}, que l'on retrouve au niveau de chaque description de migration.
En explorant les paquets qui se trouvent au niveau des répertoires et en analysant les dépendances décrites au niveau de chaque action de migration, on arrive au schéma suivant, qui est un graph dirigé acyclique:
\includegraphics{images/db/migrations_auth_admin_contenttypes_sessions.png}
\section{Sous le capot}
Une migration consiste à appliquer un ensemble de modifications (ou
\textbf{opérations}), qui exercent un ensemble de transformations, pour
que le schéma de base de données corresponde au modèle de l'application
sous-jacente.
Les migrations (comprendre les "\emph{migrations du schéma de base de
données}") sont intimement liées à la représentation d'un contexte
fonctionnel: l'ajout d'une nouvelle information, d'un nouveau champ ou
d'une nouvelle fonction peut s'accompagner de tables de données à mettre
à jour ou de champs à étendre. Il est primordial que la structure de la
base de données corresponde à ce à quoi l'application s'attend, sans
quoi la probabilité que l'utilisateur tombe sur une erreur de type
\texttt{django.db.utils.OperationalError} est (très) grande.
Typiquement, après avoir ajouté un nouveau champ \texttt{summary} à
chacun de nos livres, et sans avoir appliqué de migrations, nous tombons
sur ceci:
\begin{verbatim}
>>> from library.models import Book
>>> Book.objects.all()
Traceback (most recent call last):
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/sqlite3/base.py", line 416, in execute
return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such column: library_book.summary
\end{verbatim}
Pour éviter ce type d'erreurs, il est impératif que les nouvelles
migrations soient appliquées \textbf{avant} que le code ne soit déployé;
l'idéal étant que ces deux opérations soient réalisées de manière
atomique, avec un \emph{rollback} si une anomalie était détectée.
Pour éviter ce type d'erreurs, plusieurs stratégies peuvent être
appliquées:
TODO intégrer ici un point sur les updates db - voir designing data-intensive
applications.
Toujours dans une optique de centralisation, les migrations sont
directement embarquées au niveau du code, et doivent faire partie du
dépôt central de sources. Le développeur s'occupe de créer les
migrations en fonction des actions à entreprendre; ces migrations
peuvent être retravaillées, \emph{squashées}, \ldots\hspace{0pt} et
feront partie intégrante du processus de mise à jour de l'application.
A noter que les migrations n'appliqueront de modifications que si le
schéma est impacté. Ajouter une propriété \texttt{related\_name} sur une
ForeignKey n'engendrera aucune nouvelle action de migration, puisque ce
type d'action ne s'applique que sur l'ORM, et pas directement sur la
base de données: au niveau des tables, rien ne change. Seul le code et
le modèle sont impactés.
Une migration est donc une classe Python, présentant \emph{a minima}
deux propriétés:
\begin{enumerate}
\item
\texttt{dependencies}, qui décrit les opérations précédentes devant obligatoirement avoir été appliquées
\item
\texttt{operations}, qui consiste à décrire précisément ce qui doit être exécuté.
\end{enumerate}
Pour reprendre notre exemple d'ajout d'un champ \texttt{description} sur le modèle \texttt{WishList}, la migration ressemblera à ceci:
\begin{minted}{python}
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('gwift', '0004_name_value'),
]
operations = [
migrations.AddField(
model_name='wishlist',
name='description',
field=models.TextField(default="", null=True)
preserve_default=False,
),
]
\end{minted}
\section{Liste des migrations appliquées}
L'option \texttt{showmigrations} de \texttt{manage.py} permet de lister
toutes les migrations du projet, et d'identifier celles qui n'auraient
pas encore été appliquées:
\begin{verbatim}
$ python manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
[X] 0010_alter_group_name_max_length
[X] 0011_update_proxy_permissions
[X] 0012_alter_user_first_name_max_length
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
library
[X] 0001_initial
[X] 0002_remove_book_category_book_category
[ ] 0003_book_summary
sessions
[X] 0001_initial
\end{verbatim}
Nous voyons que parmi toutes les migrations déjà enregistrées au niveau du projet, seule la migration \texttt{0003\_book\_summary} n'a pas encore été appliquée sur ce schéma-ci.
\section{Squash}
Finalement, lorsque vous développez sur votre propre branche (cf. \protect\hyperlink{git}{???}), vous serez peut-être tentés de créer plusieurs migrations en fonction de l'évolution de ce que vous mettez en place. Dans ce cas précis, il peut être intéressant d'utiliser la méthode \texttt{squashmigrations}, qui permet \emph{d'aplatir} plusieurs fichiers en un seul.
Nous partons dans deux migrations suivantes:
\begin{minted}{python}
# library/migrations/0002_remove_book_category.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.Category'),
),
]
\end{minted}
\begin{minted}{python}
# library/migrations/0003_book_summary.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0002_remove_book_category_book_category'),
]
operations = [
migrations.AddField(
model_name='book',
name='summary',
field=models.TextField(blank=True),
),
]
\end{minted}
La commande \texttt{python\ manage.py\ squashmigrations\ library\ 0002\ 0003} appliquera une fusion entre les migrations numérotées \texttt{0002} et \texttt{0003}:
\begin{verbatim}
$ python manage.py squashmigrations library 0002 0003
Will squash the following migrations:
- 0002_remove_book_category_book_category
- 0003_book_summary
Do you wish to proceed? [yN] y
Optimizing...
No optimizations possible.
Created new squashed migration
/home/fred/Sources/gwlib/library/migrations/0002_remove_book_category_book_cat
egory_squashed_0003_book_summary.py
You should commit this migration but leave the old ones in place;
the new migration will be used for new installs. Once you are sure
all instances of the codebase have applied the migrations you squashed,
you can delete them
\end{verbatim}
Dans le cas où vous développez proprement (bis), il est sauf de purement et simplement supprimer les anciens fichiers; dans le cas où il pourrait exister au moins une instance ayant appliqué ces migrations, les anciens
\textbf{ne peuvent surtout pas être modifiés}.
Nous avons à présent un nouveau fichier intitulé \texttt{0002\_remove\_book\_category\_book\_category\_squashed\_0003\_book\_summary}:
\begin{minted}{python}
$ cat
library/migrations/0002_remove_book_category_book_category_squashed_0003_book_
summary.py
# Generated by Django 4.0.3 on 2022-03-15 18:01
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
('library', '0002_remove_book_category_book_category'),
('library', '0003_book_summary')]
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.category'),
),
migrations.AddField(
model_name='book',
name='summary',
field=models.TextField(blank=True),
),
]
\end{minted}
\section{Réinitialisation de migrations}
\href{https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html}{reset
migrations}.
En résumé:
\begin{enumerate}
\item Soit on supprime toutes les migrations (en conservant le fichier \texttt{\_\_init\_\_.py})
\item Soit on réinitialise proprement les migrations avec un \texttt{--fake-initial} (sous réserve que toutes les personnes qui utilisent déjà le projet s'y conforment\ldots Ce qui n'est pas gagné.
\end{enumerate}

887
chapters/models.tex Executable file
View File

@ -0,0 +1,887 @@
\chapter{Modélisation}
Ce chapitre aborde la modélisation des objets et les options qui y sont liées.
Avec Django, la modélisation est en lien direct avec la conception et le stockage, sous forme d'une base de données relationnelle, et la manière dont ces données s'agencent et communiquent entre elles.
Cette modélisation va ériger les premières pierres de votre édifice.
Comme expliqué par Aurélie Jean \cite{other_side}, "\emph{toute modélisation reste une approximation de la réalité}".
\begin{quote}
\emph{Le modèle n'est qu'une grande hypothèse.
Il se base sur des choix conscients et inconscients, et dans chacun de ces choix se cachent nos propres perceptions qui résultent de qui nous sommes, de nos connaissances, de nos profils scientifiques et de tant d'autres choses.} \cite{other_side}
\end{quote}
Django utilise un paradigme de persistence des données de type \href{https://fr.wikipedia.org/wiki/Mapping_objet-relationnel}{ORM} - c'est-à-dire que chaque type d'objet manipulé peut s'apparenter à une
table SQL, tout en respectant une approche propre à la programmation orientée objet.
Plus spécifiquement, l'ORM de Django suit le patron de conception \href{https://en.wikipedia.org/wiki/Active_record_pattern}{Active Records}, comme le font par exemple \href{https://rubyonrails.org/}{Rails} pour Ruby ou \href{https://docs.microsoft.com/fr-fr/ef/}{EntityFramework} pour .Net.
Le modèle de données de Django est sans doute la seule partie qui soit tellement couplée au framework qu'un changement à ce niveau nécessitera une refonte complète de beaucoup d'autres briques de vos projets; là où un pattern de type \href{https://www.martinfowler.com/eaaCatalog/repository.html}{Repository} permettrait justement de découpler le modèle des données de l'accès à ces mêmes données, un pattern Active Record lie de manière extrêmement forte le modèle à sa persistence.
Architecturalement, c'est sans doute la plus grosse faiblesse de Django, à tel point que \textbf{ne pas utiliser cette brique de fonctionnalités} peut remettre en question le choix du framework. \footnote{Et dans ce cas, il y a des alternatives comme Flask qui permettent une flexibilité et un choix des composants beaucoup plus grand.}
Conceptuellement, c'est pourtant la manière de faire qui permettra d'avoir quelque chose à présenter très rapidement: à partir du moment où vous aurez un modèle de données, vous aurez accès, grâce à cet ORM à:
\begin{enumerate}
\item
Des migrations de données et la possibilité de faire évoluer votre modèle,
\item
Une abstraction entre votre modélisation et la manière dont les données sont représentées \emph{via} un moteur de base de données relationnelles,
\item
Une interface d'administration auto-générée
\item
Un mécanisme de formulaires HTML complet, pratique à utiliser, orienté objet et logique à faire évoluer,
\item
Une définition des notions d'héritage (tout en restant dans une forme d'héritage simple).
\end{enumerate}
Comme tout ceci reste au niveau du code, cela suit également la méthodologie des douze facteurs concernant la minimisation des divergences entre environnements d'exécution: il n'est plus nécessaire d'avoir un DBA qui doive démarrer un script sur un serveur au moment de la mise à jour, de recevoir une release note de 512 pages en PDF reprenant les modifications ou de nécessiter l'intervention de trois équipes différentes lors d'une modification majeure du code.
Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande.
\section{Active Records}
Il est important de noter que l'implémentation d'Active Records reste une forme hybride entre une structure de données brutes et une classe: \cite{clean_code}
\begin{itemize}
\item
Une \textbf{classe} va exposer ses données derrière une forme d'abstraction et n'exposer que les fonctions qui opèrent sur ces données,
\item
Une \textbf{structure de données} ne va exposer que ses champs et propriétés, et ne va pas avoir de functions significatives.
\end{itemize}
L'exemple ci-dessous présente trois structure de données, qui exposent chacune leurs propres champs:
\begin{minted}{Python}
class Square:
def __init__(self, top_left, side):
self.top_left = top_left
self.side = side
class Rectangle:
def __init__(self, top_left, height, width):
self.top_left = top_left
self.height = height
self.width = width
class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius
\end{minted}
Si nous souhaitons ajouter une fonctionnalité permettant de calculer l'aire pour chacune de ces structures, nous aurons deux possibilités:
\begin{enumerate}
\item
Soit ajouter une classe de \emph{visite} qui ajoute cette fonction de calcul d'aire
\item
Soit modifier notre modèle pour que chaque structure hérite d'une classe de type \texttt{Shape}, qui implémentera elle-même ce calcul d'aire.
\end{enumerate}
Dans le premier cas, nous pouvons procéder de la manière suivante:
\begin{minted}{python}
class Geometry:
PI = 3.141592653589793
def area(self, shape):
if isinstance(shape, Square):
return shape.side * shape.side
if isinstance(shape, Rectangle):
return shape.height * shape.width
if isinstance(shape, Circle):
return PI * shape.radius**2
raise NoSuchShapeException()
\end{minted}
Dans le second cas, l'implémentation pourrait évoluer de la manière suivante:
\begin{minted}{python}
class Shape:
def area(self):
pass
class Square(Shape):
def __init__(self, top_left, side):
self.__top_left = top_left
self.__side = side
def area(self):
return self.__side * self.__side
class Rectangle(Shape):
def __init__(self, top_left, height, width):
self.__top_left = top_left
self.__height = height
self.__width = width
def area(self):
return self.__height * self.__width
class Circle(Shape):
def __init__(self, center, radius):
self.__center = center
self.__radius = radius
def area(self):
PI = 3.141592653589793
return PI * self.__radius**2
\end{minted}
Une structure de données peut être rendue abstraite au travers des notions de programmation orientée objet.
Dans l'exemple géométrique ci-dessus, repris de \cite[pp. 95-97]{clean_code}, l'accessibilité des champs devient restreinte, tandis que la fonction \texttt{area()} bascule comme méthode d'instance plutôt que de l'isoler au niveau d'un visiteur.
Nous ajoutons une abstraction au niveau des formes grâce à un héritage sur la classe \texttt{Shape}; indépendamment de ce que nous manipulerons, nous aurons la possibilité de calculer son aire.
Une structure de données permet de facilement gérer des champs et des propriétés, tandis qu'une classe gère et facilite l'ajout de fonctions et de méthodes.
Le problème d'Active Records est que chaque classe s'apparente à une table SQL et revient donc à gérer des \emph{DTO} ou \emph{Data Transfer Object}, c'est-à-dire des objets de correspondance pure et simple entre
les champs de la base de données et les propriétés de la programmation orientée objet, c'est-à-dire également des classes sans fonctions.
Or, chaque classe a également la possibilité d'exposer des possibilités d'interactions au niveau de la persistence, en \href{https://docs.djangoproject.com/en/stable/ref/models/instances/\#django.db.models.Model.save}{enregistrant ses propres données} ou en en autorisant leur \href{https://docs.djangoproject.com/en/stable/ref/models/instances/\#deleting-objects}{suppression}.
Nous arrivons alors à un modèle hybride, mélangeant des structures de données et des classes d'abstraction, ce qui restera parfaitement viable tant que l'on garde ces principes en tête et que l'on se prépare à une
éventuelle réécriture du code.
Lors de l'analyse d'une classe de modèle, nous pouvons voir que Django exige un héritage de la classe \texttt{django.db.models.Model}.
Nous pouvons regarder les propriétés définies dans cette classe en analysant le fichier
\texttt{lib\textbackslash{}site-packages\textbackslash{}django\textbackslash{}models\textbackslash{}base.py}.
Outre que \texttt{models.Model} hérite de \texttt{ModelBase} au travers de \href{https://pypi.python.org/pypi/six}{six} pour la rétrocompatibilité vers Python 2.7, cet héritage apporte notamment les fonctions \texttt{save()}, \texttt{clean()}, \texttt{delete()}, \ldots
En résumé, toutes les méthodes qui font qu'une instance sait \textbf{comment} interagir avec la base de données.
\section{Types de champs, relations et clés étrangères}
Nous l'avons vu plus tôt, Python est un langage dynamique et fortement typé.
Django, de son côté, ajoute une couche de typage statique exigé par le lien sous-jacent avec les moteurs de base de données relationnelles.
Dans ce domaine, un point d'attention est de toujours disposer d'une clé primaire pour nos enregistrements.
Si aucune clé primaire n'est spécifiée, Django s'occupera d'en ajouter une automatiquement et la nommera (par
convention) \texttt{id}.
Par défaut, et si aucune propriété ne dispose d'un attribut \texttt{primary\_key=True}, Django s'occupera d'ajouter un champ \texttt{id} grâce à son héritage de la classe \texttt{models.Model}.
Elle sera ainsi accessible autant par cette propriété que par la propriété \texttt{pk}.
Chaque champ du modèle est donc typé et lié, soit à un primitif, soit à une autre instance au travers de sa clé d'identification.
Grâce à toutes ces informations, nous sommes en mesure de représenter facilement des relations, par exemple des livres liés à des catégories:
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
\end{minted}
Les autres champs nous permettent d'identifier une catégorie (\texttt{Category}) par un nom (\texttt{name}), tandis qu'un livre (\texttt{Book}) le sera par ses propriétés \texttt{title} et une clé de relation vers une catégorie. Un livre est donc lié à une catégorie, tandis qu'une catégorie est associée à plusieurs livres.
\includegraphics{diagrams/books-foreign-keys-example.drawio.png}
A présent que notre structure dispose de sa modélisation, il nous faut informer le moteur de base de données de créer la structure correspondance, grâce à la création d'une étape de migration:
\begin{verbatim}
$ python manage.py makemigrations
Migrations for 'library':
library/migrations/0001_initial.py
- Create model Category
- Create model Book
\end{verbatim}
Cette étape créera un fichier différentiel, explicitant les modifications à appliquer à la structure de données pour rester en corrélation avec la modélisation de notre application.
Nous pouvons écrire un premier code d'initialisation de la manière suivante:
\begin{minted}{python}
from library.models import Book, Category
movies = Category.objects.create(name="Adaptations au cinéma")
medieval = Category.objects.create(name="Médiéval-Fantastique")
science_fiction = Category.objects.create(name="Sciences-fiction")
computers = Category.objects.create(name="Sciences Informatiques")
books = {
"Harry Potter": movies,
"The Great Gatsby": movies,
"Dune": science_fiction,
"H2G2": science_fiction,
"Ender's Game": science_fiction,
"Le seigneur des anneaux": medieval,
"L'Assassin Royal", medieval,
"Clean code": computers,
"Designing Data-Intensive Applications": computers
}
for book_title, category in books.items:
Book.objects.create(name=book_title, category=category)
\end{minted}
Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs catégories:
\begin{itemize}
\item
\emph{Dune} a été adapté au cinéma en 1973 et en 2021, de même que \emph{Le Seigneur des Anneaux}.
Ces deux titres (au moins) peuvent appartenir à deux catégories distinctes.
\item
Pour \emph{The Great Gatsby}, c'est l'inverse: nous l'avons initialement classé comme film, mais le livre existe depuis 1925.
\item
Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie supplémentaire "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga \emph{Harry Potter} et ses dérivés.
\end{itemize}
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour qu'une occurrence d'un livre puisse être liée à plusieurs catégories.
Au lieu d'utiliser un champ de type \texttt{ForeignKey}, nous utiliserons à présent un champ de type \texttt{ManyToMany}, c'est-à-dire qu'un livre pourra être lié à plusieurs catégories, et qu'inversément, une même catégorie pourra être liée à plusieurs livres.
\begin{minted}{python}
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category)
\end{minted}
Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition.
\begin{memorizebox}
Lorsque nous avons une relation de type \texttt{A -> B <- C}, avec des \# de 1 à N, l'intérêt sera surtout de pouvoir définir un lien \texttt{N-N} entre les tables \texttt{A} et \texttt{C} (et donc, une classe de liaison, avec ses propres champs).
Attention qu'il faudra malgré tout sauver les instances de type \texttt{A} et \texttt{C} avant de pouvoir sauver une instance de {B}.
De la même manière, si vous souhaitez accéder aux champs de \texttt{B}, il ne sera pas possible de le faire en partant de \texttt{A} ou \texttt{C}.
Il conviendra également de spécifier l'attribut \texttt{through\_fields}, afin de mettre cette abstraction en place.
\end{memorizebox}
\section{Shell}
Le \texttt{shell} est un environnement REPL \index{REPL} identique à ce que l'interpréteur Python offre par défaut, connecté à la base de données, qui permet de :
\begin{enumerate}
\item Tester des comportements spécifiques
\item Instancier des enregistrements provenant de la base de données
\item Voire, exceptionnellement, d'analyser un soucis en production.
\end{enumerate}
Il se démarre grâce à la commande \texttt{python manage.py shell}, et donne un accès intuitif \footnote{Pour un développeur\ldots} à l'ensemble des informations disponibles.
\subsection{Accès aux relations}
\begin{minted}{python}
# wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist)
\end{minted}
Depuis le code, à partir de l'instance de la classe \texttt{Item}, on peut donc accéder à la liste en appelant la propriété \texttt{wishlist} de notre instance. \textbf{A contrario}, depuis une instance de type
\texttt{Wishlist}, on peut accéder à tous les éléments liés grâce à \texttt{\textless{}nom\ de\ la\ propriété\textgreater{}\_set}; ici \texttt{item\_set}.
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut \texttt{related\_name} afin de nommer la relation inverse.
\begin{minted}{python}
# wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist, related_name='items')
\end{minted}
Si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse.
Pour palier à ce problème, nous fixons une valeur à l'attribut \texttt{related\_name}. Par facilité (et par conventions), prenez l'habitude de toujours ajouter cet attribut: votre modèle gagnera en cohérence et en lisibilité. Si cette relation inverse n'est pas nécessaire, il est possible de l'indiquer (par convention) au travers de l'attribut \texttt{related\_name="+"}.
A partir de maintenant, nous pouvons accéder à nos propriétés de la manière suivante:
\begin{verbatim}
# python manage.py shell
>>> from wish.models import Wishlist, Item
>>> wishlist = Wishlist.create('Liste de test', 'description')
>>> item = Item.create('Element de test', 'description', w)
>>>
>>> item.wishlist
<Wishlist: Wishlist object>
>>>
>>> wishlist.items.all()
[<Item: Item object>]
\end{verbatim}
\subsection{Choix}
Voir \href{https://girlthatlovestocode.com/django-model}{ici}
\begin{minted}{python}
class Runner(models.Model):
# this is new:
class Zone(models.IntegerChoices):
ZONE_1 = 1, 'Less than 3.10'
ZONE_2 = 2, 'Less than 3.25'
ZONE_3 = 3, 'Less than 3.45'
ZONE_4 = 4, 'Less than 4 hours'
ZONE_5 = 5, 'More than 4 hours'
name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField()
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
start_zone = models.PositiveSmallIntegerField( # this is new
choices=Zone.choices,
default=Zone.ZONE_5,
help_text="What was your best time on the marathon in last 2 years?"
)
\end{minted}
\section{Validateurs}
La validation des champs intervient sur toute donnée entrée via le modèle.
Cette validation dispose de trois niveaux:
\begin{enumerate}
\item Directement au niveau du modèle, au travers de validateurs sur les champs
\item Via une méthode de nettoyage sur un champ, qui permet de prendre d'autres informations contextuelles en considération
\item Via une méthode de nettoyage globale, associée à l'instance.
\end{enumerate}
\subsection{Validation d'un champ}
La première manière de valider le contenu d'un champ est aussi la plus simple.
En prenant un modèle type:
\begin{minted}{python}
from django.db import models
def validate_title(value):
if 't' not in value:
raise ValidationError('Title does not start with T')
class Book(models.Model):
title = models.CharField(max_length=255, validators=[validate_title])
\end{minted}
\subsection{Clean\_<field\_name>}
Ca, c'est le deuxième niveau. Le contexte donne accès à déjà plus d'informations et permet de valider les informations en interogeant (par exemple) la base de données.
\begin{minted}{python}
class Bidule(models.Model):
def clean_title(self):
raise ValidationError('Title does not start with T')
\end{minted}
Il n'est plus nécessaire de définir d'attribut \texttt{validators=[\ldots]}, puisque Django va appliquer un peu d'introspection pour récupérer toutes les méthodes qui commencent par \texttt{clean\_} et pour les faire correspondre au nom du champ à valider (ici, \texttt{title}).
\subsection{Clean}
Ici, c'est global: nous pouvons valider des données ou des champs globalement vis-à-vis d'autres champs déjà remplis.
\begin{minted}{python}
class Bidule(models.Model):
def clean(self):
raise ValidationError('Title does not start with T')
\end{minted}
\section{Constructeurs}
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode \texttt{\_\_init\_\_}: créez plutôt une méthode static de type \texttt{create()}, en y associant les paramètres obligatoires ou souhaités.
Mieux encore: nous pouvons passer par un \texttt{ModelManager} pour limiter le couplage; l'accès à une information stockée en base de données ne se fait dès lors qu'au travers de cette instance et pas directement au travers du modèle.
\begin{minted}{python}
class ItemManager(...):
(de mémoire, je ne sais plus exactement)
\end{minted}
\section{Jointures, compositions et filtres}
Pour appliquer une jointure sur un modèle, nous pouvons passer par les méthodes \texttt{select\_related} et \texttt{prefetch\_related}.
Il faut cependant faire \textbf{très} attention au prefetch related, qui fonctionne en fait comme une grosse requête dans laquelle nous trouvons un \texttt{IN\ (\ldots)}.
Càd que Django va récupérer tous les objets demandés initialement par le queryset, pour ensuite prendre
toutes les clés primaires, pour finalement faire une deuxième requête et récupérer les relations externes.
Au final, si votre premier queryset est relativement grand (nous parlons de 1000 à 2000 éléments, en fonction du moteur de base de données), la seconde requête va planter et vous obtiendrez une exception de type \texttt{django.db.utils.OperationalError:\ too\ many\ SQL\ variables}.
Nous pourrions penser qu'utiliser un itérateur permettrait de combiner les deux, mais ce n'est pas le cas\ldots
Comme l'indique la documentation:
\begin{verbatim}
Note that if you use iterator() to run the query, prefetch_related() calls will be ignored since these two optimizations do not make sense together.
\end{verbatim}
Ajouter un itérateur va en fait forcer le code à parcourir chaque élément de la liste, pour l'évaluer. Il y aura donc (à nouveau) autant de requêtes qu'il y a d'éléments, ce que nous cherchons à éviter.
\begin{minted}{python}
informations = (
<MyObject>.objects.filter(<my_criteria>)
.select_related(<related_field>)
.prefetch_related(<related_field>)
.iterator(chunk_size=1000)
)
\end{minted}
\begin{dangerbox}
Les requêtes sont sensibles à la casse, \textbf{même} si le moteur de base de données ne l'est pas.
C'est notamment le cas pour Microsoft SQL Server: faire une recherche directement via les outils de
Microsoft ne retournera pas obligatoirement les mêmes résultats que les managers, qui seront beaucoup plus tatillons sur la qualité des recherches par rapport aux filtres paramétrés en entrée.
\end{dangerbox}
\begin{verbatim}
Pour un `AND`, il suffit de chaîner les conditions. ** trouver un exemple ici ** :-)
\end{verbatim}
\begin{verbatim}
Mais en gros : bidule.objects.filter(condition1, condition2)
\end{verbatim}
\begin{verbatim}
Il existe deux autres options : combiner deux querysets avec l'opérateur `&` ou combiner des Q objects avec ce même opérateur.
\end{verbatim}
Soit encore combiner des filtres:
\begin{minted}{python}
from core.models import Wish
Wish.objects
Wish.objects.filter(name__icontains="test").filter(name__icontains="too")
\end{minted}
\begin{itemize}
\item
Ca, c'est notre manager.
\item
Et là, on chaîne les requêtes pour composer une recherche sur tous les souhaits dont le nom contient (avec une casse insensible) la chaîne "test" et dont le nom contient la chaîne "too".
\end{itemize}
Pour un 'OR', on a deux options :
\begin{enumerate}
\item
Soit passer par deux querysets, typiuqment
\texttt{queryset1\ \textbar{}\ queryset2}
\item
Soit passer par des \texttt{Q\ objects}, que l'on trouve dans le
namespace \texttt{django.db.models}.
\end{enumerate}
\begin{minted}{python}
from django.db.models import Q
condition1 = Q(...)
condition2 = Q(...)
bidule.objects.filter(condition1 | condition2)
\end{minted}
L'opérateur inverse (\emph{NOT})
Idem que ci-dessus : soit on utilise la méthode \texttt{exclude} sur le
queryset, soit l'opérateur \texttt{\textasciitilde{}} sur un Q object;
\subsection{N+1 Queries}
\begin{itemize}
\item
\url{http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm}
\item
\url{https://docs.djangoproject.com/en/1.9/ref/models/querysets/\#django.db.models.query.QuerySet.iterator}
\item
\url{http://blog.etianen.com/blog/2013/06/08/django-querysets/}
\end{itemize}
Deux solutions:
\begin{enumerate}
\item
Prefetch
\item
select\_related
\end{enumerate}
\subsection{Indices}
Après analyse seulement.
\subsection{Agrégation et annotations}
\url{https://docs.djangoproject.com/en/3.1/topics/db/aggregation/}
\section{Métamodèle et introspection}
Comme chaque classe héritant de \texttt{models.Model} possède une propriété \texttt{objects}.
Cette propriété permet d'accéder aux objects persistants dans la base de données, au travers d'un \texttt{ModelManager}.
Les propriétés de la classe Meta les plus utiles sont les suivates:
\begin{itemize}
\item
\texttt{ordering} pour spécifier un ordre de récupération spécifique.
\item
\texttt{verbose\_name} pour indiquer le nom à utiliser au singulier
pour définir votre classe
\item
\texttt{verbose\_name\_plural}, pour le pluriel.
\item
\texttt{contraints} (Voir \href{https://girlthatlovestocode.com/django-model}{ici}-), par exemple
\end{itemize}
\subsection{Ordre par défaut}
\begin{minted}{python}
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',)
\end{minted}
Nous définissons un ordre par défaut, directement au niveau du modèle.
Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre (la méthode \texttt{order\_by} existe et peut être chaînée à n'importe quel \emph{queryset}).
D'où l'intérêt de tester ce type de comportement, dans la mesure où un \texttt{top\ 1} dans votre code pourrait être modifié simplement par cette petite information.
Pour sélectionner un objet au pif: \texttt{return\ Category.objects.order\_by("?").first()}
En plus de cela, il faut bien tenir compte des propriétés \texttt{Meta} de la classe: si elle contient déjà un ordre par défaut, celui-ci sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe.
Cela signifie que le \texttt{top 1} utilisé dans le code pourrait être impacté en cas de modification de cette propriété \texttt{ordering}, ce qui corrobore la nécessité de \emph{tester le code dans son ensemble}.
\subsection{Représentation textuelle}
verbose\_name et verbose\_name\_plural + lien avec
\subsection{Contraintes}
Les contraintes sont de plusieurs types:
\begin{enumerate}
\item Unicité
\item Composée
\end{enumerate}
\begin{minted}{python}
constraints = [ # constraints added
models.CheckConstraint(check=models.Q(year_born__lte=datetime.date
.today().year-18), name='will_be_of_age'),
]
\end{minted}
\section{Querysets et managers}
L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants:
\begin{itemize}
\item
Les \texttt{managers}, qui consistent en un point d'entrée pour
accéder aux objets persistants
\item
Les \texttt{querysets}, qui permettent de filtrer des ensembles ou
sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour
ajouter d'autres filtres à des filtres existants, et fonctionnent
comme un super jeu d'abstraction pour accéder à nos données
(persistentes).
\end{itemize}
Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut \texttt{objects}, qui correspond à un manager (ou un gestionnaire, si vous préférez).
Ce gestionnaire constitue l'interface par laquelle vous accéderez à la base de données.
Mais pour cela, vous aurez aussi besoin d'appliquer certains requêtes ou filtres.
Et pour cela, vous aurez besoin des \texttt{querysets}, qui consistent en des ensembles de requêtes.
\subsection{Managers}
Les \texttt{managers} constituent une partie de la couche de "services" de notre application.
Ces services peuvent se trouver à trois endroits \cite[Rule \# 2]{django_for_startup_founders}:
\begin{enumerate}
\item Dans les modèles ou au niveau des managers
\item Dans les forms ou les sérializeurs
\item Dans une couche de services séparée du reste.
\end{enumerate}
\subsection{Querysets}
Si on veut connaître la requête SQL sous-jacente à l'exécution du queryset, il suffit d'appeler la fonction str() sur la propriété \texttt{query}:
\begin{verbatim}
queryset = Wishlist.objects.all()
print(queryset.query)
\end{verbatim}
Chaque définition de modèle utilise un \texttt{Manager}, afin d'accéder
à la base de données et traiter nos demandes. Indirectement, une
instance de modèle ne \textbf{connait} \textbf{pas} la base de données:
c'est son gestionnaire qui a cette tâche. Il existe deux exceptions à
cette règle: les méthodes \texttt{save()} et \texttt{update()}.
\begin{itemize}
\item
Instanciation: MyClass()
\item
Récupération: MyClass.objects.get(pk=\ldots\hspace{0pt})
\item
Sauvegarde : MyClass().save()
\item
Création: MyClass.objects.create(\ldots\hspace{0pt})
\item
Liste des enregistrements: MyClass.objects.all()
\end{itemize}
Par défaut, le gestionnaire est accessible au travers de la propriété
\texttt{objects}. Cette propriété a une double utilité:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Elle est facile à surcharger - il nous suffit de définir une nouvelle
classe héritant de ModelManager, puis de définir, au niveau de la
classe, une nouvelle assignation à la propriété \texttt{objects}
\item
Il est tout aussi facile de définir d'autres propriétés présentant des
filtres bien spécifiques.
\end{enumerate}
\section{Services}
Une des recommandations que l'on peut lire pour django consiste à créer des \textit{fat models} et à conserver des \textit{thin views}, c'est-à-dire à compléter le maximum d'informations directement au niveau du modèle et sortir le maximum de règles métiers des vues.
Le soucis, c'est que l'on arrive rapidement à créer des classes de plusieurs centaines de lignes, qui dialoguent directement avec la base de données, et qui pourraient potentiellement:
\begin{enumerate}
\item
Réaliser des requêtes N+1 vers d'autres objets
\item
Ajouter un couplage supplémentaire directement au niveau des propriétés.
\end{enumerate}
\begin{quote}
The "Fat Models" recommendation is one of the most destructive in my opinion: https://django-best-practices.readthedocs.io/en/latest/appli..., along with Django Rest Framework "Model Serializers". A JSON serializer that talks directly to the database is just madness.
-- \url{https://news.ycombinator.com/item?id=23322880}
\end{quote}
La proposition consiste à créer des services qui viennent se glisser entre les vues et les managers.
De cette manière, nous :
\begin{enumerate}
\item
Conservons le principe de \textit{thin views}
\item
Ajoutons un concept intermédiaire, entre ces vues et les managers, afin de limiter la maintenance des requêtes, mais aussi
\item
Gardons les managers comme couche d'accès à la base de données,
\item
Conservons des modèles cohérents, dans lesquels il serait logique de trouver une propriété spécifique,
\item
Gardons les modèles à la frontière entre la représentation mentale d'un concept et sa presistance.
\end{enumerate}
\includegraphics{images/diagrams/views-services-managers-models.drawio.png}
\section{Refactoring et héritages}
On constate que plusieurs classes possèdent les mêmes propriétés \texttt{created\_at} et \texttt{updated\_at}, initialisées aux mêmes valeurs.
Pour gagner en cohérence, nous allons créer une classe dans laquelle nous définirons ces deux champs, et nous ferons en sorte que les classes \texttt{Wishlist}, \texttt{Item} et \texttt{Part} en
héritent.
Django gère trois sortes d'héritage:
\begin{itemize}
\item
L'héritage par classe abstraite
\item
L'héritage classique
\item
L'héritage par classe proxy.
\end{itemize}
\subsection{Classes abstraites}
L'héritage par classe abstraite consiste à déterminer une classe mère
qui ne sera jamais instanciée. C'est utile pour définir des champs qui
se répèteront dans plusieurs autres classes et surtout pour respecter le
principe de DRY. Comme la classe mère ne sera jamais instanciée, ces
champs seront en fait dupliqués physiquement, et traduits en SQL, dans
chacune des classes filles.
\begin{minted}{python}
# wish/models.py
class AbstractModel(models.Model):
class Meta:
abstract = True
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Wishlist(AbstractModel):
pass
class Item(AbstractModel):
pass
class Part(AbstractModel):
pass
\end{minted}
En traduisant ceci en SQL, on aura en fait trois tables, chacune
reprenant les champs \texttt{created\_at} et \texttt{updated\_at}, ainsi
que son propre identifiant:
\begin{minted}{sql}
--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_wishlist" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_item" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_part" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
COMMIT;
\end{minted}
Une bonne pratique consiste à ajouter un "autre" identifiant pour un modèle susceptible d'être interrogé.
Nous pensons ici à un UUID ou à un slug \index{slug}, qui permettrait d'avoir une sorte de clé sémantique associée à un object; ceci évite également d'avoir des identifiants qui soient trop facilement récupérables.
\subsection{Héritage classique}
L'héritage classique est généralement déconseillé, car il peut
introduire très rapidement un problème de performances: en reprenant
l'exemple introduit avec l'héritage par classe abstraite, et en omettant
l'attribut \texttt{abstract\ =\ True}, on se retrouvera en fait avec
quatre tables SQL:
\begin{itemize}
\item
Une table \texttt{AbstractModel}, qui reprend les deux champs
\texttt{created\_at} et \texttt{updated\_at}
\item
Une table \texttt{Wishlist}
\item
Une table \texttt{Item}
\item
Une table \texttt{Part}.
\end{itemize}
A nouveau, en analysant la sortie SQL de cette modélisation, on obtient
ceci:
\begin{minted}{sql}
--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_abstractmodel" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_wishlist" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES
"wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_item" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES
"wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_part" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES
"wish_abstractmodel" ("id")
)
;
COMMIT;
\end{minted}
Le problème est que les identifiants seront définis et incrémentés au
niveau de la table mère. Pour obtenir les informations héritées, nous
seront obligés de faire une jointure. En gros, impossible d'obtenir les
données complètes pour l'une des classes de notre travail de base sans
effectuer un \textbf{join} sur la classe mère.
Dans ce sens, cela va encore\ldots\hspace{0pt} Mais imaginez que vous
définissiez une classe \texttt{Wishlist}, de laquelle héritent les
classes \texttt{ChristmasWishlist} et \texttt{EasterWishlist}: pour
obtenir la liste complètes des listes de souhaits, il vous faudra faire
une jointure \textbf{externe} sur chacune des tables possibles, avant
même d'avoir commencé à remplir vos données. Il est parfois nécessaire
de passer par cette modélisation, mais en étant conscient des risques
inhérents.
\subsection{Classes Proxy}
Lorsqu'on définit une classe de type \textbf{proxy}, on fait en sorte
que cette nouvelle classe ne définisse aucun nouveau champ sur la classe
mère. Cela ne change dès lors rien à la traduction du modèle de données
en SQL, puisque la classe mère sera traduite par une table, et la classe
fille ira récupérer les mêmes informations dans la même table: elle ne
fera qu'ajouter ou modifier un comportement dynamiquement, sans ajouter
d'emplacements de stockage supplémentaires.
Nous pourrions ainsi définir les classes suivantes:
\begin{minted}{python}
# wish/models.py
class Wishlist(models.Model):
name = models.CharField(max_length=255)
description = models.CharField(max_length=2000)
expiration_date = models.DateField()
@staticmethod
def create(self, name, description, expiration_date=None):
wishlist = Wishlist()
wishlist.name = name
wishlist.description = description
wishlist.expiration_date = expiration_date
wishlist.save()
return wishlist
class ChristmasWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
christmas = datetime(current_year, 12, 31)
w = Wishlist.create(name, description, christmas)
w.save()
class EasterWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
expiration_date = datetime(current_year, 4, 1)
w = Wishlist.create(name, description, expiration_date)
w.save()
\end{minted}
\section{Conclusions}
Le modèle proposé par Django est un composant extrêmement performant, mais fort couplé avec le coeur du framework.
Si tous les composants peuvent être échangés avec quelques manipulations, le cas du modèle sera
plus difficile à interchanger.
A côté de cela, il permet énormément de choses, et vous fera gagner un temps précieux, tant en rapidité d'essais/erreurs, que de preuves de concept.
Dans les exemples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (\textbf{ForeignKey}) d'une classe A vers une classe B.
Pour représenter d'autres types de relations, il existe également les champs de type \textbf{ManyToManyField}, afin de représenter une relation N-N.
Il existe également un type de champ spécial pour les clés étrangères, qui est le Les champs de type \textbf{OneToOneField}, pour représenter une relation 1-1.

741
chapters/new-project.tex Executable file
View File

@ -0,0 +1,741 @@
\chapter{Démarrer un nouveau projet}
Django fonctionne sur un
\href{https://docs.djangoproject.com/en/dev/internals/release-process/}{roulement de trois versions mineures pour une version majeure}, clôturé par une version LTS (\emph{Long Term Support}).
\section{Gestion des dépendances}
Comme nous en avons déjà discuté, Poetry est la solution que nous avons choisie pour la gestion de nos dépendances.
Pour installer une nouvelle librairie, vous pouvez simplement passer par la commande \texttt{pip\ install\ \textless{}my\_awesome\_library\textgreater{}}.
Dans le cas de Django, et après avoir activé l'environnement, nous pouvons à présent y installer Django. Comme expliqué ci-dessus, la librairie restera indépendante du reste du système, et ne polluera aucun autre projet. nous exécuterons donc la commande suivante:
\begin{verbatim}
$ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift-
env/Scrips/activate.bat pour Windows.
$ pip install django
Collecting django
Downloading Django-3.1.4
100% |################################|
Installing collected packages: django
Successfully installed django-3.1.4
\end{verbatim}
Ici, la commande \texttt{pip\ install\ django} récupère la \textbf{dernière version connue disponible dans les dépôts \url{https://pypi.org/}} (sauf si vous en avez définis d'autres. Mais c'est hors sujet).
Nous en avons déjà discuté : il est important de bien spécifier la version que vous souhaitez utiliser, sans quoi vous risquez de rencontrer des effets de bord.
L'installation de Django a ajouté un nouvel exécutable: \texttt{django-admin}, que l'on peut utiliser pour créer notre nouvel espace de travail.
Par la suite, nous utiliserons \texttt{manage.py}, qui constitue un \textbf{wrapper} autour de \texttt{django-admin}.
Pour démarrer notre projet, nous lançons \texttt{django-admin\ startproject\ gwift}:
\begin{verbatim}
$ django-admin startproject gwift
\end{verbatim}
Cette action a pour effet de créer un nouveau dossier \texttt{gwift}, dans lequel nous trouvons la structure suivante :
\begin{verbatim}
$ tree gwift
gwift
-- gwift
----- asgi.py
----- __init__.py
----- settings.py
----- urls.py
----- wsgi.py
-- manage.py
\end{verbatim}
C'est dans ce répertoire que vont vivre tous les fichiers liés au projet.
Le but est de faire en sorte que toutes les opérations (maintenance, déploiement, écriture, tests, \ldots\hspace{0pt}) puissent se faire à partir d'un seul point d'entrée.
L'utilité de ces fichiers est définie ci-dessous:
\begin{itemize}
\item
\texttt{settings.py} contient tous les paramètres globaux à notre projet.
\item
\texttt{urls.py} contient les variables de routes, les adresses utilisées et les fonctions vers lesquelles elles pointent.
\item
\texttt{manage.py}, pour toutes les commandes de gestion.
\item
\texttt{asgi.py} contient la définition de l'interface \href{https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface}{ASGI}, le protocole pour la passerelle asynchrone entre votre application et le serveur Web.
\item
\texttt{wsgi.py} contient la définition de l'interface \href{https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface}{WSGI}, qui permettra à votre serveur Web (Nginx, Apache, \ldots\hspace{0pt}) de faire un pont vers votre projet.
\end{itemize}
Indiquer qu'il est possible d'avoir plusieurs structures de dossiers et qu'il n'y a pas de "magie" derrière toutes ces commandes.
La seule condition est que les chemins référencés soient cohérents par rapport à la structure sous-jacente.
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous stockerons les dépendances et un fichier README:
TODO
Comme nous venons d'ajouter une dépendance à notre projet, profitons-en pour créer un fichier reprenant tous les dépendances de notre projet.
Celles-ci sont normalement placées dans un fichier \texttt{requirements.txt}.
Dans un premier temps, ce fichier peut être placé directement à la racine du projet, mais on préférera rapidement le déplacer dans un sous-répertoire spécifique (\texttt{requirements}), afin de grouper les dépendances en fonction de leur environnement de destination:
\begin{itemize}
\item
\texttt{base.txt}
\item
\texttt{dev.txt}
\item
\texttt{production.txt}
\end{itemize}
Au début de chaque fichier, il suffit d'ajouter la ligne \texttt{-r\ base.txt}, puis de lancer l'installation grâce à un \texttt{pip\ install\ -r\ \textless{}nom\ du\ fichier\textgreater{}}.
De cette manière, il est tout à fait acceptable de n'installer \texttt{flake8} et \texttt{django-debug-toolbar} qu'en développement par exemple.
Dans l'immédiat, nous allons ajouter \texttt{django} dans une version égale à la version 3.2 dans le fichier \texttt{requirements/base.txt}.
\begin{verbatim}
$ echo 'django==3.2' > requirements/base.txt
$ echo '-r base.txt' > requirements/prod.txt
$ echo '-r base.txt' > requirements/dev.txt
\end{verbatim}
Une bonne pratique consiste à également placer un fichier \texttt{requirements.txt} à la racine du projet, et dans lequel nous retrouverons le contenu \texttt{-r requirements/production.txt} (notamment pour Heroku).
Prenez directement l'habitude de spécifier la version ou les versions compatibles: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets.
Pour être sûr et certain le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances.
Entre deux versions d'une même librairie, des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc.
Il suffit de parcourirles pages de \emph{Changements incompatibles avec les anciennes versions dans Django}
\href{https://docs.djangoproject.com/fr/3.1/releases/3.0/}{(par exemple ici pour le passage de la 3.0 à la 3.1)} pour réaliser que certaines opérations ne sont pas anodines, et que sans filet de sécurité, c'est le
mur assuré.
Avec les mécanismes d'intégration continue et de tests unitaires, nous verrons plus loin comment se prémunir d'un changement inattendu.
\includegraphics{images/django-support-lts.png}
La version utilisée sera une bonne indication à prendre en considération pour nos dépendances, puisqu'en visant une version particulière, nous ne devrons pratiquement pas nous soucier (bon, un peu quand même, mais nous le verrons plus tard\ldots\hspace{0pt}) des dépendances à installer, pour peu que l'on reste sous un certain seuil.
Dans les étapes ci-dessous, nous épinglerons une version LTS afin de nous assurer une certaine sérénité d'esprit (= dont nous ne occuperons pas pendant les 3 prochaines années).
\section{Django}
Comme nous l'avons vu ci-dessus, \texttt{django-admin} permet de créer un nouveau projet.
Nous faisons ici une distinction entre un \textbf{projet} et une \textbf{application}:
\begin{itemize}
\item
\textbf{Un projet} représente l'ensemble des applications, paramètres, middlewares, dépendances, \ldots, qui font que votre code fait ce qu'il est sensé faire.
Il s'agit grosso modo d'un câblage de tous les composants entre eux.
\item
\textbf{Une application} est un contexte d'exécution (vues, comportements, pages HTML, \ldots), idéalement autonome, d'une partie du projet.
Une application est supposée avoir une portée de réutilisation, même s'il ne sera pas toujours possible de viser une généricité parfaite.
\end{itemize}
Pour \texttt{gwift}, nous aurons :
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-gwift.png}
\caption{Projet Django vs Applications}
\end{figure}
\begin{enumerate}
\item
Une première application pour la gestion des listes de souhaits et des éléments,
\item
Une deuxième application pour la gestion des utilisateurs,
\item
Voire une troisième application qui gérera les partages entre utilisateurs et listes.
\end{enumerate}
Nous voyons également que la gestion des listes de souhaits et éléments aura besoin de la gestion des utilisateurs - elle n'est pas autonome -, tandis que la gestion des utilisateurs n'a aucune autre dépendance
qu'elle-même.
Pour \texttt{khana}, nous pourrions avoir quelque chose comme ceci:
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-khana.png}
\caption{Django Project vs Applications}
\end{figure}
En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la gestion des utilisateurs et la possibilité qu'ils auront de communiquer entre eux.
Ceci pourrait être commun aux deux projets.
Nous pouvons clairement visualiser le principe de \textbf{contexte} pour une application: celle-ci viendra avec son modèle, ses tests, ses vues et son paramétrage et pourrait ainsi être réutilisée dans un autre projet.
C'est en ça que consistent les \href{https://www.djangopackages.com/}{paquets Django} déjà disponibles:
ce sont "\emph{simplement}" de petites applications empaquetées et pouvant être réutilisées dans différents contextes (eg. \href{https://github.com/tomchristie/django-rest-framework}{Django-Rest-Framework}, \href{https://github.com/django-debug-toolbar/django-debug-toolbar}{Django-Debug-Toolbar}, \ldots
Le projet s'occupe principalement d'appliquer une couche de glue entre différentes applications.
Découper proprement un projet en plusieurs applications totalement autonomes est illusoire.
Une bonne pratique consiste à rester pragmatique et à partir avec \textbf{une seule} application, et la découper lorsque vous jugerez qu'elle grossit trop ou trop rapidement \cite[Rule \#5 : don't split files by default]{django_for_startup_founders} : découper trop rapidement et sans raison valable une application en plein de petits fichiers va gâcher énormément de temps de développement, sans apporter de réels bénéfices.
D'autre part, une (autre) bonne pratique consiste à aussi \textbf{limiter à cinq} le nombre de modèles différents dans chaque application.
Tant que ce seuil ne sera pas atteint, laissez ce principe de côté.
\subsection{manage.py}
Le fichier \texttt{manage.py} que vous trouvez à la racine de votre projet est un \textbf{wrapper} sur les commandes \texttt{django-admin}.
A partir de maintenant, nous n'utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet :
\begin{itemize}
\item
\texttt{manage.py\ check} pour vérifier (en surface\ldots\hspace{0pt})
que votre projet ne rencontre aucune erreur évidente
\item
\texttt{manage.py\ check\ -\/-deploy}, pour vérifier (en surface
aussi) que l'application est prête pour un déploiement
\item
\texttt{manage.py\ runserver} pour lancer un serveur de développement
\item
\texttt{manage.py\ test} pour découvrir les tests unitaires
disponibles et les lancer.
\end{itemize}
La liste complète peut être affichée avec \texttt{manage.py\ help}. Vous
remarquerez que ces commandes sont groupées selon différentes
catégories:
\begin{itemize}
\item
\textbf{auth}: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
\item
\textbf{django}: vérifier la \textbf{conformité} du projet, lancer un \textbf{shell}, \textbf{dumper} les données de la base, effectuer une migration du schéma, \ldots
Ce sont des commandes d'administration générale.
\item
\textbf{sessions}: suppressions des sessions en cours
\item
\textbf{staticfiles}: gestion des fichiers statiques et lancement du serveur de développement.
\end{itemize}
Chaque section correspond à une application.
En analysant le code, ces applications peuvent être trouvées sous \texttt{django.contrib}.
Nous verrons plus tard comment ajouter de nouvelles commandes.
Si nous démarrons la commande \texttt{python\ manage.py\ runserver},
nous verrons la sortie console suivante:
\begin{verbatim}
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
[...]
December 15, 2020 - 20:45:07
Django version 3.1.4, using settings 'gwift.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
\end{verbatim}
Si nous nous rendons sur la page \url{http://127.0.0.1:8000} (ou \url{http://localhost:8000}) comme le propose si gentiment notre (nouveau) meilleur ami, nous verrons ceci:
\begin{figure}
\centering
\includegraphics{images/django/manage-runserver.png}
\caption{python manage.py runserver (Non, ce n'est pas Challenger)}
\end{figure}
Nous avons mis un morceau de la sortie console entre crochet \texttt{{[}\ldots{}\hspace{0pt}{]}} ci-dessus, car elle concerne les migrations.
Si vous avez suivi les étapes jusqu'ici, vous avez également dû voir un message type \texttt{You\ have\ 18\ unapplied\ migration(s).\ {[}\ldots{}\hspace{0pt}{]}\ Run\ \textquotesingle{}python\ manage.py\ migrate\textquotesingle{}\ to\ apply\ them.}
Cela concerne les migrations, et c'est un point que nous verrons un peu plus tard.
\section{Minimal Viable Application}
Maintenant que nous avons a vu à quoi servait \texttt{manage.py}, nous pouvons créer notre nouvelle application grâce à la commande \texttt{manage.py\ startapp\ \textless{}label\textgreater{}}.
Notre première application servira à structurer des listes de souhaits, les éléments qui les composent et les pourcentages de participation que chaque utilisateur aura souhaité offrir.
De manière générale, essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc \texttt{wish}.
\begin{verbatim}
$ python manage.py startapp wish
\end{verbatim}
Résultat? Django nous a créé un répertoire \texttt{wish}, dans lequel nous trouvons les fichiers et dossiers suivants:
\begin{itemize}
\item
\texttt{wish/\_\_init\_\_.py} pour que notre répertoire \texttt{wish} soit converti en package Python.
\item
\texttt{wish/admin.py} servira à structurer l'administration de notre application.
Chaque information peut être gérée facilement au travers d'une interface générée à la volée par le framework.
Nous y reviendrons par la suite.
\item
\texttt{wish/apps.py} qui contient la configuration de l'application et qui permet notamment de fixer un nom ou un libellé \url{https://docs.djangoproject.com/en/stable/ref/applications/}
\item
\texttt{wish/migrations/} est le dossier dans lequel seront stockées toutes les différentes migrations de notre application (= toutes les modifications que nous apporterons aux données que nous souhaiterons manipuler)
\item
\texttt{wish/models.py} représentera et structurera nos données. Ce modèle est intimement lié aux migrations.
\item
\texttt{wish/tests.py} pour les tests unitaires.
\end{itemize}
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire \texttt{wish} dans votre répertoire \texttt{gwift} existant.
C'est \emph{une} forme de convention.
La structure de vos répertoires devient celle-ci:
TODO
Notre application a bien été créée, et nous l'avons déplacée dans le répertoire \texttt{gwift} ! \footnote{Il manque quelques fichiers utiles, qui seront décrits par la suite, pour qu'une application soit réellement autonome: templates, \texttt{urls.py}, managers, services, \ldots}
\section{Fonctionnement général}
Le métier de programmeur est devenu de plus en plus complexe.
Il y a 20 ans, nous pouvions nous contenter d'une simple page PHP dans laquelle nous mixions l'ensemble des actions à réaliser : requêtes en bases de données, construction de la page, \ldots
La recherche d'une solution à un problème n'était pas spécialement plus complexe - dans la mesure où le rendu des enregistrements en direct n'était finalement qu'une forme un chouia plus évoluée du \texttt{print()} ou des \texttt{System.out.println()} - mais c'était l'évolutivité des applications qui en prenait un coup: une grosse partie des tâches étaient dupliquées entre les différentes pages, et l'ajout d'une nouvelle fonctionnalité était relativement ardue.
Django (et d'autres frameworks) résolvent ce problème en se basant ouvertement sur le principe de \texttt{Dont\ repeat\ yourself} \footnote{DRY}.
Chaque morceau de code ne doit apparaitre qu'une seule fois, afin de limiter au maximum la redite (et donc, l'application d'un même correctif à différents endroits).
Le chemin parcouru par une requête est expliqué en (petits) détails ci-dessous.
\textbf{Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application}.
Ici, nous prenons l'exemple de l'URL fictive \texttt{https://gwift/wishes/91827}.
Lorsque cette URL "arrive" dans notre application, son point d'entrée se trouvera au niveau des fichiers \texttt{asgi.py} ou \texttt{wsgi.py}.
Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu'elle va parcourir.
\begin{figure}
\centering
\includegraphics{images/diagrams/django-how-it-works.png}
\caption{How it works}
\end{figure}
\textbf{Etape 0} - La première étape consiste à vérifier que cette URL répond à un schéma que nous avons défini dans le fichier \texttt{gwift/urls.py}.
\begin{minted}{Python}
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/<int:wish_id>", wish_details),
]
\end{minted}
\textbf{Etape 1} - Si ce n'est pas le cas, l'application n'ira pas plus loin et retournera une erreur à l'utilisateur.
\textbf{Etape 2} - Django va parcourir l'ensemble des \emph{patterns} présents dans le fichier \texttt{urls.py} et s'arrêtera sur le premier qui correspondra à la requête qu'il a reçue.
Ce cas est relativement trivial: la requête \texttt{/wishes/91827} a une correspondance au
niveau de la ligne \texttt{path("wishes/\textless{}int:wish\_id\textgreater{}} dans l'exemple ci-dessous. Django va alors appeler la fonction \footnote{Qui ne sera pas toujours une fonction. Django s'attend à trouver un \emph{callable}, c'est-à-dire n'importe quel élément qu'il peut appeler comme une fonction.} associée à ce \emph{pattern}, c'est-à-dire \texttt{wish\_details} du module \texttt{gwift.views}.
\begin{itemize}
\item
Nous importons la fonction \texttt{wish\_details} du module
\texttt{gwift.views}
\item
Champomy et cotillons! Nous avons une correspondance avec
\texttt{wishes/details/91827}
\end{itemize}
Le module \texttt{gwift.views} qui se trouve dans le fichier \texttt{gwift/views.py} peut ressembler à ceci:
\begin{minted}{Python}
# gwift/views.py
[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"generated_at": datetime.now()
}
return render(
request,
"wish_details.html",
context
)
\end{minted}
Pour résumer, cette fonction permet:
\begin{enumerate}
\item
De construire un \emph{contexte}, qui est représenté sous la forme d'un dictionnaire associant des clés à des valeurs.
Les clés sont respectivement \texttt{user\_name}, \texttt{user\_first\_name} et \texttt{now}, tandis que leurs valeurs respectives sont \texttt{Bond}, \texttt{James} et le \texttt{moment\ présent} \footnote{Non, pas celui d'Eckhart Tolle}.
\item
Nous passons ensuite ce dictionnaire à un canevas, \texttt{wish\_details.html}, que l'on trouve normalement dans le répertoire \texttt{templates} de notre projet, ou dans le répertoire \texttt{templates} propre à notre application.
\item
L'application du contexte sur le canevas au travers de la fonction \texttt{render} nous donne un résultat formaté.
\end{enumerate}
\begin{minted}{html}
<!-- fichier wish_details.html -->
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.</p>
<p>This page was generated at {{ generated_at }}</p>
</body>
</html>
\end{minted}
Après application de notre contexte sur ce template, nous obtiendrons ce document, qui sera renvoyé au navigateur de l'utilisateur qui aura fait la requête initiale:
\begin{minted}{html}
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<h1>Hi!</h1>
<p>My name is Bond. James Bond.</p>
<p>This page was generated at 2027-03-19 19:47:38</p>
</body>
</html>
\end{minted}
\begin{figure}
\centering
\includegraphics{images/django/django-first-template.png}
\caption{Résultat}
\end{figure}
\section{Configuration globale}
\subsection{Structure finale}
En repartant de la structure initiale décrite au chapitre précédent, nous arrivons à ceci.
TODO : passer à poetry
\subsection{Cookie-cutter}
Pfiou! Ca en fait des commandes et du boulot pour "juste" démarrer un nouveau projet, non? Sachant qu'en plus, nous avons dû modifier des fichiers, déplacer des dossiers, ajouter des dépendances, configurer une
base de données, \ldots
Bonne nouvelle! Il existe des générateurs, permettant de démarrer rapidement un nouveau projet sans (trop) se prendre la tête.
Le plus connu (et le plus personnalisable) est \href{https://cookiecutter.readthedocs.io/}{Cookie-Cutter}, qui se base sur des canevas \emph{type \href{https://pypi.org/project/Jinja2/}{Jinja2}}, pour créer une
arborescence de dossiers et fichiers conformes à votre manière de travailler.
Et si vous avez la flemme de créer votre propre canevas, vous pouvez utiliser \href{https://cookiecutter-django.readthedocs.io}{ceux qui existent déjà}.
Pour démarrer, créez un environnement virtuel (comme d'habitude):
\begin{verbatim}
python -m venv .venvs\cookie-cutter-khana
.venvs\cookie-cutter-khana\Scripts\activate.bat
(cookie-cutter-khana) $ pip install cookiecutter
Collecting cookiecutter
[...]
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 arrow-0.17.0
binaryornot-0.4.4 certifi-2020.12.5 chardet-4.0.0 click-7.1.2 cookiecutter-
1.7.2 idna-2.10 jinja2-time-0.2.0 poyo-0.5.0 python-dateutil-2.8.1 python-
slugify-4.0.1 requests-2.25.1 six-1.15.0 text-unidecode-1.3 urllib3-1.26.2
(cookie-cutter-khana) $ cookiecutter https://github.com/pydanny/cookiecutter-
django
[...]
[SUCCESS]: Project initialized, keep up the good work!
\end{verbatim}
Si vous explorez les différents fichiers, vous trouverez beaucoup de similitudes avec la configuration que nous vous proposions ci-dessus.
En fonction de votre expérience, vous serez tenté de modifier certains paramètres, pour faire correspondre ces sources avec votre utilisation ou vos habitudes.
Il est aussi possible d'utiliser l'argument \texttt{-\/-template}, suivie d'un argument reprenant le nom de votre projet (\texttt{\textless{}my\_project\textgreater{}}), lors de l'initialisation d'un projet avec la commande \texttt{startproject} de \texttt{django-admin}, afin de calquer votre arborescence sur un projet
existant.
La \href{https://docs.djangoproject.com/en/stable/ref/django-admin/\#startproject}{documentation} à ce sujet est assez complète.
\begin{verbatim}
django-admin.py startproject --template=https://[...].zip <my_project>
\end{verbatim}
\section{Tests unitaires}
Il y a deux manières d'écrire les tests: soit avant, soit après l'implémentation.
Oui, idéalement, les tests doivent être écrits à l'avance. Entre nous, on ne va pas râler si vous faites l'inverse, l'important étant que vous le fassiez. Une bonne métrique pour vérifier l'avancement des tests est la couverture de code.
Chaque application est créée par défaut avec un fichier \textbf{tests.py}, qui inclut la classe \texttt{TestCase} depuis le package \texttt{django.test}:
On a deux choix ici:
\begin{enumerate}
\item Utiliser les librairies de test de Django
\item Utiliser Pytest
\end{enumerate}
\subsection{django.test}
\begin{listing}[H]
\begin{minted}{Python}
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
\end{minted}
\end{listing}
\subsection{Pytest}
\subsection{Couverture de code}
Quel que soit le framework de tests choisi (django-tests, pytest, unittest, \ldots), la couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests.
Il ne s'agit pas de vérifier que le code est bien testé, mais de vérifier quelle partie du code est testée.
Le paquet coverage se charge dévaluer le pourcentage de code couvert par les tests.
Avec pytest, il convient dutiliser le paquet pytest-cov, suivi de la commande pytest
\texttt{--cov=gwift tests/}.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer par le paquet django-coverage-plugin.
Ajoutez-le dans le fichier requirements/base.txt, et lancez une couverture de code grâce à la commande coverage.
La configuration peut se faire dans un fichier .coveragerc que vous placerez à la racine de votre projet, et qui sera lu lors de lexécution.
\begin{verbatim}
# requirements/base.text
[...]
django_coverage_plugin
\end{verbatim}
\begin{verbatim}
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
\end{verbatim}
Nous pouvons à présent jouer au jeu de la couverture, qui consiste à augmenter ou égaliser la couverture existante à chaque nouvelle fonctionnalité ajoutée ou bug corrigé.
De cette manière, sans arriver à une couverture de 100\%, chaque modification du code améliorera la base existante. \footnote{cf. Two Scoops of Django}.
Suivant l'outil d'intégration continue que vous utiliserez, cette évolution pourra être affichée à chaque demande de fusion, et pourra être considérée comme un indicateur de qualité.
\begin{verbatim}
$ coverage run --source "." manage.py test
$ coverage report
Name Stmts Miss Cover
---------------------------------------------
gwift\gwift\__init__.py 0 0 100%
gwift\gwift\settings.py 17 0 100%
gwift\gwift\urls.py 5 5 0%
gwift\gwift\wsgi.py 4 4 0%
gwift\manage.py 6 0 100%
gwift\wish\__init__.py 0 0 100%
gwift\wish\admin.py 1 0 100%
gwift\wish\models.py 49 16 67%
gwift\wish\tests.py 1 1 0%
gwift\wish\views.py 6 6 0%
---------------------------------------------
TOTAL 89 32 64%
----
$ coverage html
\end{verbatim}
\subsection{Recommandations sur les tests}
En résumé, il est recommandé de:
\begin{enumerate}
\item
Tester que le nommage d'une URL (son attribut \texttt{name} dans les fichiers \texttt{urls.py}) corresponde à la fonction que l'on y a définie
\item
Tester que l'URL envoie bien vers l'exécution d'une fonction (et que cette fonction est celle que l'on attend)
\end{enumerate}
\subsubsection{Tests de nommage}
\begin{minted}{python}
from django.core.urlresolvers import reverse
from django.test import TestCase
class HomeTests(TestCase):
def test_home_view_status_code(self):
url = reverse("home")
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
\end{minted}
\subsubsection{Tests d'URLs}
\begin{minted}{python}
from django.core.urlresolvers import reverse
from django.test import TestCase
from .views import home
class HomeTests(TestCase):
def test_home_view_status_code(self):
view = resolve("/")
self.assertEquals(view.func, home)
\end{minted}
\subsection{Couverture de code}
Pour l'exemple, nous allons écrire la fonction \texttt{percentage\_of\_completion} sur la classe \texttt{Wish}, et nous allons spécifier les résultats attendus avant même d'implémenter son contenu. Prenons le cas où nous écrivons la méthode avant son test:
\begin{minted}{python}
class Wish(models.Model):
[...]
@property
def percentage_of_completion(self):
"""
Calcule le pourcentage de complétion pour un élément.
"""
number_of_linked_parts = WishPart.objects.filter(wish=self).count()
total = self.number_of_parts * self.numbers_available
percentage = (number_of_linked_parts / total)
return percentage * 100
\end{minted}
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
\begin{verbatim}
$ coverage run --source "." src/manage.py test wish
$ coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 36 5 0 0 88%
------------------------------------------------------------------
TOTAL 69 5 4 1 93%
\end{verbatim}
Si vous générez le rapport HTML avec la commande \texttt{coverage\ html} et que vous ouvrez le fichier
\texttt{coverage\_html\_report/src\_wish\_models\_py.html}, vous verrez que les méthodes en rouge ne sont pas testées. \textbf{A contrario}, la couverture de code atteignait \textbf{98\%} avant l'ajout de cette
nouvelle méthode.
Pour cela, on va utiliser un fichier \texttt{tests.py} dans notre application \texttt{wish}. \textbf{A priori}, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application.
\begin{minted}{python}
from django.test import TestCase
class TestWishModel(TestCase):
def test_percentage_of_completion(self):
"""
Vérifie que le pourcentage de complétion d'un souhait
est correctement calculé.
Sur base d'un souhait, on crée quatre parts et on vérifie
que les valeurs s'étalent correctement sur 25%, 50%, 75% et 100%.
"""
wishlist = Wishlist(
name='Fake WishList',
description='This is a faked wishlist'
)
wishlist.save()
wish = Wish(
wishlist=wishlist,
name='Fake Wish',
description='This is a faked wish',
number_of_parts=4
)
wish.save()
part1 = WishPart(wish=wish, comment='part1')
part1.save()
self.assertEqual(25, wish.percentage_of_completion)
part2 = WishPart(wish=wish, comment='part2')
part2.save()
self.assertEqual(50, wish.percentage_of_completion)
part3 = WishPart(wish=wish, comment='part3')
part3.save()
self.assertEqual(75, wish.percentage_of_completion)
part4 = WishPart(wish=wish, comment='part4')
part4.save()
self.assertEqual(100, wish.percentage_of_completion)
\end{minted}
L'attribut \texttt{@property} sur la méthode \texttt{percentage\_of\_completion()} va nous permettre d'appeler directement la méthode \texttt{percentage\_of\_completion()} comme s'il s'agissait d'une propriété de la classe, au même titre que les champs \texttt{number\_of\_parts} ou \texttt{numbers\_available}. Attention que ce type de méthode contactera la base de données à chaque fois qu'elle sera appelée. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de
comportement a très peu d'impacts, mais ce n'est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de \textbf{cache}, que nous aborderons plus loin.
En relançant la couverture de code, on voit à présent que nous arrivons à 99\%:
\begin{verbatim}
$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 34 0 0 0 100%
src\wish\tests.py 20 0 0 0 100%
------------------------------------------------------------------
TOTAL 87 0 4 1 99%
\end{verbatim}
En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code.
A noter que tester le modèle en lui-même (ses attributs ou champs) ou des composants internes à Django n'a pas de sens: cela reviendrait à mettre en doute son fonctionnement interne.
Selon le principe du SRP \ref{SRP}, c'est le framework lui-même qui doit en assurer la maintenance et le bon fonctionnement.
\section{Licence}
Choisissez une licence.
Si votre projet n'en a pas, vous pourriez être tenu responsable de manquements ou de bugs collatéraux.
En cas de désastre médical ou financier, ce simple fichier peut faire toute la différence.
\textit{React, for example, has an additional clause that could potentially cause patent claim conflicts with React users} \cite[p. 47]{roads_and_bridges}.
Cette issue a été adressée en 2017 \footnote{\url{hhttps://github.com/facebook/react/issues/7293}}.
Un autre exemple concerne StackOverflow, qui utilisait une licence Creative Commons de type CC-BY-SA pour chaque contenu posté sur sa plateforme.
Cette licence est cependante limitante, dans la mesure où elle obligeait que chaque utilisateur cite l'origine du code utilisé.
Ceci n'était pas vraiment connu de tous, mais si un utilisateur qui venait à opérer selon des contraintes relativement strictes (en milieu professionnel, par exemple) venait à poser une question sur la plateforme, il aurait été légalement obligé de réattribuer la réponse qu'il aurait pu utiliser.
StackOverflow est ainsi passé vers une licence MIT présentant moins de restrictions.
Trois licences \footnote{Bien qu'il en existe beaucoup} sont généralement proposées et utilisées:
\begin{enumerate}
\item
\textbf{MIT}
\item
\textbf{GPLv3}
\item
\textbf{Fair Source}, annoncée en 2015, qui propose une solution à la nécessité de proposer une licence gratuite pour une utilisation personnelle ou en petites entreprises, tout en étant payante pour une une utilisation commerciale plus large.
\footnote{\textit{Under Fair Source, code is free to view, download, execute, and modify up to a certain number of users in an organization. After that limit is reached, the organization must pay a licencing fee, determined by the published - \url{https://fair.io}}}
\item
\textbf{WTFPL}
\end{enumerate}
Mike Perham, qui maintient Sidekiq, a ainsi proposé une forme de dualité entre la mise à disposition du code source et son utilisation \cite[p. 95]{roads_and_bridges}:
\begin{quote}
\textit{Remember: Open Source is not Free Software.
The source may be viewable on GitHub but that doesn't mean anyone can use it for any purpose.
There's no reason you can't make your source code accessible but also charge to use it.
As long as you are the owner of the code, you have the right to licence it however you want.}
\textit{...[The] reality is most smaller OSS project have a single person doing 95\% of the work.
If this is true, be grateful for unpaid help but don't feel guilty about keeping 100\% of the income.}
\end{quote}
\section{Conclusions}
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

1457
chapters/python.tex Executable file

File diff suppressed because it is too large Load Diff

9
chapters/resources.tex Executable file
View File

@ -0,0 +1,9 @@
\chapter{Annexes}
\section{Liens et librairies utiles}
\begin{itemize}
\item
`Django factory boy
\textless{}\url{https://github.com/rbarrois/django-factory_boy/tree/v1.0.0\%3E\%60_}
\end{itemize}

31
chapters/security.tex Executable file
View File

@ -0,0 +1,31 @@
\chapter{Sécurité}
\begin{quote}
New developers simply want to write code that works; they don't know how to make software secure, or they mistakenly assume that the public code they use in their software has been audited for security. \cite[p. 79]{roads_and_bridges}
\end{quote}
\begin{quote}
We put all security issues into JIRA, which all engineers use in their daily work, and they were either 'P1' or 'P2', meaning that they had to be fixed immediately or by the end of the week, even if the issue is only an internally-facing application.
Any time we had a security issue, we would conduct a post-mortem, because it would result in better educating our engineers on how to prevent it from happening again in the future, as well as a fantastic mechanism for transferring security knowledge to our engineering team.
-- Nick Galbreath \cite[p. 315]{devops_handbook}
\end{quote}
L'objectif final est d'utiliser les mêmes méthodes de travail pour les équipes de développement, pour les équipes d'opérations, et pour la sécurité de l'information, en poussant jusqu'à l'intégration de la télémétrie à chaque niveau de la chaîne de production, mais aussi en prouvant aux développeurs (en début de chaîne) qu'ils sont constamment sous attaque informatique, afin de les sensibiliser.
Il conviendra d'ajouter au dépôt de source: Au niveau du dépôt de sources, il convient d'y ajouter:
\begin{enumerate}
\item
Les librairies standards, leur utilisation et leur configuration (2FA, \ldots)
\item
Comment gérer les injections SQL, le cross-site-scripting, \ldots
\item
Comment gérer les secrets dans les applications: comment gérer les mots de passe, les journaux de logs, \ldots
\item
Les paquets à utiliser et à compiler (NTP pour synchroniser les horloges, les paramètres d'OpenSSL, \ldots), les configurations d'Nginx/Apache, \ldots
\end{enumerate}
\section{Certificats}

268
chapters/templates.tex Executable file
View File

@ -0,0 +1,268 @@
\chapter{Templates}
Avant de commencer à interagir avec nos données au travers de listes, formulaires et d'interfaces sophistiquées, quelques mots sur les templates: il s'agit en fait de \textbf{squelettes} de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher selon le format que vous définirez.
Un squelette de page HTML basique ressemble à ceci:
\begin{minted}{html}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Hello world!</p>
</body>
</html>
\end{minted}
Notre première vue permettra de récupérer la liste des objets de type \texttt{Wishlist} que nous avons définis dans le fichier \texttt{wish/models.py}.
Supposez que cette liste soit accessible \textit{via} la clé \texttt{wishlists} d'un dictionnaire passé au template.
Cette liste devient dès lors accessible grâce aux tags \texttt{\{\% for wishlist in wishlists \%\}}.
A chaque tour de boucle, nous pourrons directement accéder à la variable \texttt{\{\{ wishlist \}\}}.
De même, il sera possible d'accéder aux propriétés de cette objet de la même manière: \texttt{\{\{ wishlist.id \}\}}, \texttt{\{\{ wishlist.description \}\}}, \ldots et d'ainsi respecter la mise en page que nous souhaitons.
En reprenant l'exemple de la page HTML définie ci-dessus, nous pouvons l'agrémenter de la manière suivante:
\begin{minted}{html}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
</body>
</html>
\end{minted}
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/html/my-first-wishlists.png}}
\end{figure}
\section{Entête, héritage}
Plutôt que de réécrire à chaque fois le même entête, nous pouvons nous simplifier la vie en implémentant un héritage au niveau des templates.
Pour cela, il suffit de définir des blocs de contenu, et d'\emph{étendre} une page de base, puis de surcharger ces mêmes blocs.
Par exemple, si on repart de notre page de base ci-dessus, on va y définir deux blocs réutilisables:
\begin{minted}{html}
<!-- templates/base.html -->
{% load static %}<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>{% block title %}Gwift{% endblock %}</title> <1>
</head>
<body>
{% block body %}<p>Hello world!</p>{% endblock %} <2>
</body>
</html>
\end{minted}
Nous avons à présent un bloc \texttt{title} et un bloc \texttt{body}, qui peuvent être surchargés dès qu'une page se définit comme extension de notre page \texttt{base.html}:
\begin{minted}{html}
<!-- templates/wishlist/wishlist_list.html -->
{% extends "base.html" %}
{% block title %}{{ block.super }} - Listes de souhaits{% endblock %} <2>
{% block body %}
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
{% endblock% %}
\end{minted}
\section{Extensions}
Attention: il est primordial que les extensions/tags ne fassent aucune requête en base de données.
Il est vraiment important que les données (re)travaillées soient disponibles dès que l'appel sera réalisé, sans quoi cela dégradera énormément les performances à l'affichage de la page.
Chronologie T -> T+1 -> T+2
Données affichées: X -> X, Y -> X,Y,Z
Et ça sera dégueulasse.
\subsection{Extensions natives}
Django vient avec un ensemble de *tags* ou *template tags*.
On a vu la boucle \texttt{for} ci-dessus, mais il existe \href{https://docs.djangoproject.com/fr/1.9/ref/templates/builtins/}{beaucoup d'autres tags nativement présents}.
Les principaux sont par exemple:
\begin{itemize}
\item Les conditions, qui permettent de vérifier un contexte et de n'afficher le contenu d'un bloc que si la condition est vérifiée
\begin{itemize}
\item \texttt{if} \ldots \texttt{elif} \ldots \texttt{endif}
\end{itemize}
\item Les opérateurs de comparaisons:
\begin{itemize}
\item \texttt{<}
\item \texttt{>}
\item \texttt{==}
\item \texttt{in}
\item \texttt{not in}
\end{itemize}
\end{itemize}
Regroupements avec le tag `{% regroup ... by ... as ... %}`.
* `{% url %}` pour construire facilement une URL à partir de son nom
* `urlize` qui permet de remplacer des URLs trouvées dans un champ de type CharField ou TextField par un lien cliquable.
* ...
Chacune de ces fonctions peut être utilisée autant au niveau des templates qu'au niveau du code.
Il suffit d'aller les chercher dans le package \texttt{django.template.defaultfilters}.
Par exemple:
\begin{minted}{python}
from django.db import models
from django.template.defaultfilters import urlize
class Suggestion(models.Model):
"""Représentation des suggestions.
"""
subject = models.TextField(verbose_name="Sujet")
def urlized_subject(self):
"""
Voir https://docs.djangoproject.com/fr/3.0/howto/custom-template-tags/
"""
return urlize(self.subject, autoescape=True)
\end{minted}
\subsubsection{Regroup by}
\subsubsection{Inclusion}
\subsection{Extensions non-natives}
En plus des quelques tags survolés ci-dessus, il est également possible de construire ses propres tags. La structure est un peu bizarre, car elle consiste à ajouter un paquet dans une de vos applications, à y définir un nouveau module et à y définir un ensemble de fonctions.
Chacune de ces fonctions correspondra à un tag appelable depuis vos templates.
Il existe trois types de tags *non-builtins*:
\begin{enumerate}
\item *Les filtres* - on peut les appeler grâce au *pipe* `|` directement après une valeur dans le template.
\item *Les tags simples* - ils peuvent prendre une valeur ou plusieurs en paramètre et retourne une nouvelle valeur. Pour les appeler, c'est *via* les tags \texttt{\{\% nom\_de\_la\_fonction param1 param2 \ldots \%\}}.
\item *Les tags d'inclusion*: ils retournent un contexte (ie. un dictionnaire), qui est ensuite passé à un nouveau template. Type \texttt{\{\% include '...' ... \%\}}.
\end{enumerate}
Pour l'implémentation:
1. On prend l'application `wish` et on y ajoute un répertoire `templatetags`, ainsi qu'un fichier `\_\_init\_\_.py`.
2. Dans ce nouveau paquet, on ajoute un nouveau module que l'on va appeler `tools.py`
3. Dans ce module, pour avoir un aperçu des possibilités, on va définir trois fonctions (une pour chaque type de tags possible).
\subsubsection{Filtres}
\begin{minted}{python}
# wish/tools.py
from django import template
from wish.models import Wishlist
register = template.Library()
@register.filter(is_safe=True)
def add_xx(value):
return '%sxx' % value
\end{minted}
\subsubsection{Tags simples}
Un \textbf{tag simple} reçoit une valeur ou un objet en entrée et génère une valeur de retour simple (un objet, un type natif, ...).
\begin{minted}{python}
# wish/tools.py
from django import template
from wish.models import Wishlist
register = template.Library()
@register.simple_tag
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
\end{minted}
\subsubsection{Tags d'inclusion}
Les \textit{tags d'inclusion} sont des tags associés à un (morceau de) template.
C'est-à-dire qu'une fois qu'ils auront réalisés le traitement qui leur est demandé, il généreront un canevas HTML qui sera \textbf{inclus} à l'endroit où le tag aura été appelé.
\begin{minted}{python}
# wish/tools.py
from django import template
from wish.models import Wishlist
register = template.Library()
@register.inclusion_tag('wish/templatetags/wishlists_list.html')
def wishlists_list():
return { 'list': Wishlist.objects.all() }
\end{minted}
\subsection{Pagination}
\section{Structure et configuration}
Il est conseillé que les templates respectent la structure de vos différentes applications, mais dans un répertoire à part.
Par convention, nous les placerons dans un répertoire \texttt{templates}.
La hiérarchie des fichiers devient alors celle-ci:
Par défaut, Django cherchera les templates dans les répertoirer d'installation.
Vous devrez vous éditer le fichier \texttt{gwift/settings.py} et ajouter, dans la variable \texttt{TEMPLATES}, la clé \texttt{DIRS} de la manière suivante:
\begin{minted}{python}
TEMPLATES = [
{
...
'DIRS': [ 'templates' ],
...
},
]
\end{minted}
\subsection{Fichiers statiques}
\section{Dynamisme - HTMX}
Voir ici https://github.com/spookylukey/django-htmx-patterns
https://www.mattlayman.com/blog/2021/how-to-htmx-django/
https://github.com/spookylukey/django-htmx-patterns/blob/master/approach.rst
https://github.com/spookylukey/django-htmx-patterns/blob/master/base\_template.rst
https://github.com/spookylukey/django-htmx-patterns/blob/master/headers.rst
https://github.com/spookylukey/django-htmx-patterns/blob/master/posts.rst
https://github.com/spookylukey/django-htmx-patterns/blob/master/separate\_partials.rst
https://github.com/spookylukey/django-htmx-patterns/blob/master/separate\_partials\_single\_view.rst
https://github.com/spookylukey/django-htmx-patterns/blob/master/inline\_partials.rst

233
chapters/tests.tex Executable file
View File

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

5
chapters/thanks.tex Executable file
View File

@ -0,0 +1,5 @@
\chapter{Remerciements}
$\Phi\phi$ pour son soutien, son entrain, nos discussions, ses remarques, ses idées et sa disponibilité.
Ced', pour son impulsion

304
chapters/tools.tex Executable file
View File

@ -0,0 +1,304 @@
\chapter{Outils de développement}
\section{Environnement de développement intégré \index{IDE}}
Concrètement, nous pourrions tout à fait nous limiter à Notepad ou Notepad++.
Mais à moins d'aimer se fouetter avec un câble USB, nous apprécions la complétion du code, la coloration syntaxique, l'intégration des tests unitaires et d'un debugger, ainsi que deux-trois sucreries qui feront plaisir à n'importe quel développeur.
Si vous manquez d'idées ou si vous ne savez pas par où commencer:
\begin{itemize}
\item
\href{https://vscodium.com/}{VSCodium}, avec les plugins
\href{https://marketplace.visualstudio.com/items?itemName=ms-python.python}{Python}et
\href{https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens}{GitLens}
\item
\href{https://www.jetbrains.com/pycharm/}{PyCharm}
\item
\href{https://www.vim.org/}{Vim} avec les plugins
\href{https://github.com/davidhalter/jedi-vim}{Jedi-Vim} et
\href{https://github.com/preservim/nerdtree}{nerdtree}
\end{itemize}
Si vous hésitez, et même si Codium n'est pas le plus léger (la faute à \href{https://www.electronjs.org/}{Electron}\ldots\hspace{0pt}), il fera correctement son travail (à savoir : faciliter le vôtre), en intégrant suffisament de fonctionnalités qui gâteront les papilles émoustillées du développeur impatient.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/environment/codium.png}}
\caption{Codium en action}
\end{figure}
\subsection{Configuration}
VSCodium permet également de définir certaines clés de configuration globalement ou au niveau d'un projet.
Celle qui nous semble critique, indispensable et primordial est la suppression automatique des espaces en fin de ligne, qui facilite énormément la relecture de \textit{commits} et la collaboration entre intervenants.
\begin{minted}{js}
/* fichier placé habituellement dans ~/.config/code/User/settings.json */
{
[...]
"files.trimTrailingWhitespace": true,
}
\end{minted}
Nous pouvons également noter la possibilité d'installer des greffons (dont un gestionnaire d'interpréteur Python), l'intégration poussée de Git \index{git} ou l'énorme support de plein de langages de programmation ou de mise en forme: Go, XML, HTML, Java, C\#, PHP, LaTeX, \ldots, ainsi que tout l'atirail d'aides à l'utilisation de Docker.
\subsection{Mode debug}
Le débugger intégré vous permettra de spécifier le point d'entrée à utiliser, de placer des points d'arrêt (éventuellement conditionnés par certaines valeurs attribuées à des variables), de lancer une analyse sur les variables et contextes, sur la pile d'appels, ...
Bref: de savoir où vous en êtes et pourquoi vous êtes là.
Ce débugger fonctionne soit sur le fichier courant (déconseillé), soit \textit{via} un fichier \texttt{launch.json} que vous pourrez placer dans le répertoire \texttt{.vscode} placé à la racine de votre projet.
Pour une application Django, ce fichier contiendra par exemple la configuration suivante:
\begin{minted}{js}
{
"version": "0.2.0",
"configurations": [
{
"name": "Python Django",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": [
"runserver"
],
"django": true,
"justMyCode": true
}
]
}
\end{minted}
Les \textit{configurations} peuvent être multiples, et nous pouvons voir que l'objectif de cette configuration-ci est de démarrer la commande \texttt{\$\{workspaceFolder\}/manage.py}, avec le paramètre \texttt{runserver}, ce qui aura pour effet de démarrer l'exécution du code dans une enveloppe sécurisée et plombée de capteurs en tout genre qui faciliteront énormément votre travail de recherche et d'introspection du code.
\section{Terminal}
\emph{A priori}, les IDE proposés ci-dessus fournissent par défaut ou \emph{via} des greffons un terminal intégré.
Ceci dit, disposer d'un terminal séparé facilite parfois certaines tâches.
A nouveau, si vous manquez d'idées :
\begin{enumerate}
\item
Soit vous utilisez celui qui intégré à VSCodium et qui fera suffisament bien son travail
\item
Si vous êtes sous Windows, téléchargez une copie de \href{https://cmder.net/}{Cmder}.
Il n'est pas le plus rapide, mais propose une intégration des outils Unix communs (\texttt{ls}, \texttt{pwd}, \texttt{grep}, \texttt{ssh}, \texttt{git}, \ldots\hspace{0pt}) sans trop se fouler.
\item
Pour tout autre système, vous devriez disposer en natif de ce qu'il faut.
\end{enumerate}
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/environment/terminal.png}}
\end{figure}
\section{Un gestionnaire de mots de passe}
Nous en auront besoin pour gé(né)rer des phrases secrètes pour nos applications.
Si vous n'en utilisez pas déjà un, partez sur \href{https://keepassxc.org/}{KeepassXC}: il est multi-plateformes, suivi et s'intègre correctement aux différents environnements, tout en restant accessible.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/environment/keepass.png}}
\end{figure}
La finalité de cette application va être de centraliser la gestion de vos phrases de passe, pour les accès aux bases de données, services en ligne, etc.
Il existe des alternatives, comme Bitwarden, qui proposent des services libres, gratuits ou payants.
\section{Un système de gestion de versions}
Il existe plusieurs systèmes de gestion de versions.
Le plus connu/utilisé à l'heure actuelle est \href{https://git-scm.com/}{Git}, notamment pour sa (très) grande flexibilité et sa rapidité d'exécution.\footnote{L'adoption massive de Git a rendu la coopération beaucoup facile sur de nombreux projets: avant Git (et Github, qui a popularisé Git), chaque projet utilisait un système de contrôle de version différent. A présent, savoir contribuer à un projet permet de contribuer à tous les projets \cite[p.69]{roads_and_bridges}}
Il est une aide précieuse pour développer rapidement des preuves de concept, switcher vers une nouvelle fonctionnalité, un bogue à réparer ou une nouvelle release à proposer au téléchargement.
Ses deux plus gros défauts concernent sa courbe d'apprentissage pour les nouveaux venus et la complexité des actions qu'il permet de réaliser.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/xkcd-1597-git.png}}
\caption{\url{https://xkcd.com/1597/}}
\end{figure}
Même pour un développeur solitaire, un système de gestion de versions (quel qu'il soit) reste indispensable, car il permet d'isoler un ensemble de modifications dans une \textit{unité de travail}, jusqu'à ce que celles-ci forment un tout cohérent:
\begin{enumerate}
\item
Chaque "\textbf{branche}" correspond à une tâche à réaliser: un bogue à corriger (\emph{Hotfix A}), une nouvelle fonctionnalité à ajouter ou un "\emph{truc à essayer}" \footnote{Oui, comme dans "Attends, j'essaie vite un truc, si ça marche, c'est beau."} (\emph{Feature A} et \emph{Feature B}).
\item
Chaque "\textbf{commit}" correspond à une sauvegarde atomique d'un état ou d'un ensemble de modifications cohérentes entre elles.\footnote{Il convient donc de s'abstenir de modifier le CSS d'une application et la couche d'accès à la base de données, sous peine de se faire huer par ses relecteurs au prochain stand-up.}
De cette manière, il est beaucoup plus facile pour le développeur de se concenter sur un sujet en particulier, dans la mesure où celui-ci ne doit pas obligatoirement être clôturé pour appliquer un changement de contexte.
\end{enumerate}
L'historique d'un module est ainsi disponible, sauvé et traçable: qui a réalisé quelle modification à quel moment.
Ceci permet notamment de dessiner l'évolution du code et de mettre un surbrillance que certains modules distincts évoluent un peu trop main dans la main (et devraient donc être refactoriser, selon les principes de développement énumérés plus tôt).
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/diagrams/git-workflow.png}}
\caption{Git en action}
\end{figure}
Cas pratique: vous développez une nouvelle fonctionnalité qui va révolutionner le monde de demain et d'après-demain, quand, tout à coup (!), vous vous rendez compte que vous avez perdu votre conformité aux normes PCI parce les données des titulaires de cartes ne sont pas isolées correctement.
Il suffit alors de:
\begin{enumerate}
\item
Sauver le travail en cours (\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ {[}WIP{]}})
\item
Revenir sur la branche principale (\texttt{git\ checkout\ main})
\item
Créer un "hotfix" (\texttt{git\ checkout\ -b\ hotfix/pci-compliance})
\item
Solutionner le problème (sans doute un \texttt{;} en trop ?)
\item
Sauver le correctif sur cette branche (\texttt{git\ add\ .\ \&\&\ git\ commit\ -m\ "Did\ it!"})
\item
Récupérer ce correctif sur la branche principal (\texttt{git\ checkout\ main\ \&\&\ git\ merge\ hotfix/pci-compliance})
\item
Et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que l'univers vous réclame à cor et à a cri (\texttt{git\ checkout\ features/dinolol})
\end{enumerate}
Il existe plusieurs méthodes pour gérer ces flux d'informations.
Les plus connues sont Gitflow \footnote{\url{https://www.gitflow.com/}} et Threeflow \footnote{\url{https://www.reddit.com/r/programming/comments/7mfxo6/a_branching_strategy_simpler_than_gitflow/}}.
La gestion de versions de fichiers permet de conserver un historique de toutes les modifications enregistrées, associées à un horodatage et une description.
\begin{advicebox}
Il existe (à nouveau) énormément de tutoriels.
Pour aller un peu plus loin, Carl Chenet\footnote{\url{https://carlchenet.com/}} a ainsi écrit sept articles pour débuter avec Git:
\begin{enumerate}
\item
\href{https://carlchenet.com/debuter-avec-git-creer-un-depot/}{Créer un dépôt}
\item
\href{https://carlchenet.com/debuter-avec-git-premier-ajout-de-code/}{Premier ajout de code}
\item
\href{https://carlchenet.com/debuter-avec-git-partie-3-un-commit-plus-complexe/}{Un commit plus complexe}
\item
\href{https://carlchenet.com/debuter-avec-git-partie-4-les-commits-et-les-branches/}{Les commits et les branches}
\item
\href{https://carlchenet.com/debuter-avec-git-partie-5-fusionner-des-branches/}{Fusionner des branches}
\item
\href{https://carlchenet.com/debuter-avec-git-partie-6-une-fusion-de-branches-echoue/}{Si une fusion de branche échoute}
\item
\href{https://carlchenet.com/debuter-avec-git-partie-7-git-rebase-pour-re-ecrire-son-historique/}{Rebase pour réécrire son historique}
\end{enumerate}
\end{advicebox}
\subsection{Décrire ses changements}
La description d'un changement se fait \emph{via} la commande \texttt{git\ commit}.
Il est possible de lui passer directement le message associé à ce changement grâce à l'attribut \texttt{-m}, mais c'est une pratique relativement déconseillée: un \emph{commit} ne doit effectivement pas obligatoirement être décrit sur une seule ligne.
Une description plus complète, accompagnée des éventuels tickets ou références de tâches, sera plus complète, plus agréable à lire, et plus facile à revoir pour vos éventuels relecteurs.
De plus, la plupart des plateformes de dépôts présenteront ces informations de manière ergonomique. Par exemple:
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/environment/gitea-commit-message.png}}
\caption{Un exemple de commit affiché dans Gitea}
\end{figure}
La première ligne est reprise comme étant le titre (normalement, sur 50 caractères maximum); le reste est repris comme une description (optionnelle).
\begin{minted}{text}
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
\end{minted}
Il est possible de suivre les recommandations de \textit{Conventional Commits} \footnote{\url{https://www.conventionalcommits.org/en/v1.0.0/}}, qui ont pour objectifs de:
\begin{enumerate}
\item
Générer automatiquement un \textit{CHANGELOG}
\item
Déterminer automatiquement des sauts de versions (en se basant sur les types de commits)
\item
Communiquer la nature des changements appliqués au code
\item
Déclencher (automatiquement, toujours) des processus de construction ou de publication
\item
Rendre l'accès au code plus lisible, en facilitant l'exploration du code au travers de commits mieux structurés.
\end{enumerate}
\subsubsection{<type>}
\subsubsection{<scope>}
\subsubsection{<description>}
\subsubsection{<body>}
\subsubsection{<footer(s)>}
\subsubsection{CHANGELOG}
Une fois les changements décrits en suivant ces conventions, et sous réserve qu'elles soient bien respectées, permet de générer un CHANGELOG \footnote{\url{https://github.com/conventional-changelog/standard-version}}.
\subsection{Nommer ses branches}
\section{Bases de données}
\begin{quote}
The database is really nothing more than a big bucket of bits where we store our data on a long term basis
\cite[p. 281]{clean_architecture}
\end{quote}
Django gère plusieurs moteurs de base de données.
Certains sont gérés nativement (PostgreSQL, MariaDB, SQLite); et \emph{a priori}, ces trois-là sont disponibles pour tous les systèmes d'exploitation.
D'autres moteurs nécessitent des librairies tierces (Oracle, Microsoft SQL Server).
\subsection{SQLite}
Parfois, SQLite peut être une bonne option:
\begin{quote}
Write througput is the area where SQLite struggles the most, but there's not a ton of compelling data online about how it fares, so I got some of my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly modified1 version of the SQLite kvtest2 program on it.
Writing 512 byte blobs as separate transactions, in WAL mode with synchronous=normal3, temp\_store=memory, and mmap enabled, I got 13.78$\mu$s per write, or \textasciitilde72,568 writes per second. Going a bit larger, at 32kb writes, I got 303.74$\mu$s per write, or \textasciitilde3,292 writes per second.
That's not astronomical, but it's certainly way more than most websites being used by humans need.
If you had 10 million daily active users, each one could get more than 600 writes per day with that.
\end{quote}
\begin{quote}
Looking at read throughput, SQLite can go pretty far: with the same test above, I got a read throughput of \textasciitilde496,770 reads/sec (2.013$\mu$s/read) for the 512 byte blob.
Other people also report similar results
--- Expensify reports that you can get 4M QPS if you're willing to make some slightly more involved changes and use a beefier server.
Four million QPS is enough that every internet user in the world could make \textasciitilde70 queries per day, with a little headroom left over.
Most websites don't need that kind of throughput. \cite{consider_sqlite}
\end{quote}
\subsection{MySQL/MariaDB}
\subsection{PostgreSQL}
\subsection{Gestionnaires}
Il n'est pas obligatoire de disposer d'une application de gestion pour ces moteurs: pour les cas d'utilisation simples, le shell Django pourra largement suffire (nous y reviendrons).
Mais pour faciliter la gestion des bases de données elles-même, et si vous n'êtes pas à l'aise avec la ligne de commande, choisissez l'une des applications d'administration ci-dessous en fonction du moteur de base de données que vous souhaitez utiliser.
\begin{itemize}
\item
Pour \textbf{PostgreSQL}, il existe \href{https://www.pgadmin.org/}{pgAdmin}
\item
Pour \textbf{MariaDB} ou \textbf{MySQL}, partez sur \href{https://www.phpmyadmin.net/}{PHPMyAdmin}
\item
Pour \textbf{SQLite}, il existe \href{https://sqlitebrowser.org/}{SQLiteBrowser} PHPMyAdmin ou PgAdmin.
\end{itemize}
\section{Intégration continue}
\subsection{Qualité du code}
Code climate, SonarQube.
\section{Insomnia}

331
chapters/trees.tex Executable file
View File

@ -0,0 +1,331 @@
\chapter{Arborescences et graphs}
Les arborescences et les graphs sont deux structures de données extrêmement utiles et souvent rencontrées dans les applications modernes.
Le cas le plus simple pourrait être un forum de discussions \footnote{Anciennement, PhpBB permettait de répondre à un message antérieur, et copiait pour cela le message en question dans le nouveau encart. Niveau traçabilité, on était plus proche du zéro absolu que de la blockchain.}, un réseau social ou une plateforme de discussions chiffrée \footnote{iMessages est sans doute plus évolué que Signal à ce sujet, mais les deux permettent bien de répondre à un message en particulier}, où un intervenant `A` a la possibilité de répondre à un message antérieurement publié.
\section{Arborescences}
Il est possible de voir une arborescence comme un graph dirigé dont chaque noeud n'aurait au maximum qu'une seule relation vers son parent.
Chaque niveau de l'arborescence est un \texttt{noeud}, tandis que les noeuds n'ayant aucun enfant sont des \texttt{feuilles}.
\begin{graphic}{images/trees/tree01.png}
\caption{Un exemple d'arborescence sur quatre niveaux}
\end{graphic}
Il existe plusieurs manières de représenter une hiérarchie ou une arborescence dans une base de données relationnelles.
Plus précisément, il existe cinq modélisations principales connues; chaque présentation présente des avantages et désavantages.
La représentation d'une structure hiérarchique peut être faite de plusieurs manières:
\begin{itemize}
\item Autant de tables qu'il y a de niveaux
\item Adjency lists
\item Path Enumeration ou \textit{jaywalking}
\item Nested sets
\item Closure trees
\end{itemize}
L'une d'entre elles sort du lot (les *Closures*) avec un tout-petit-micro-inconvénient par rapport à tous ses avantages.
\subsection{1 table = 1 niveau}
Cette solution est la plus facile à mettre en place, mais a tellement d'inconvénients qu'elle n'a pas vraiment sa place ici.
Dans le cas d'un réseau social, c'est un peu comme si nous ne pouvions répondre qu'à un certain nombre de messages, ou comme si notre arborescence était artificiellement limitée à un nombre restreint de niveaux.
Cette représentation est la plus naïve du lot: on aurait autant de tables qu'il y a de niveaux à représenter.
De cette manière, il est facile de faire des jointures entre les différentes tables.
Le problème est que chacune de ces tables aura les mêmes champs que les autres; une modification dans l'une d'entre elle devra sans doute être répercutée dans toutes les autres tables.
Si un nouveau niveau peut être ajouté, cela équivaudra à ajouter une nouvelle table (avec autant de nouvelles contraintes que celles déjà présentes pour les autres tables).
\begin{minted}[tabsize=4]{python}
# simple.models.py
from django.db import models
class FirstLevel(models.Model):
"""La racine de l'aborescence."""
name = models.CharField(max_length=50)
def breadcrumb(self):
return self.name
class SecondLevel(models.Model):
"""Le deuxième niveau.
On y trouve une propriété `parent`.
Le reste est identique à la modélisation de la racine.
"""
name = models.CharField(max_length=50)
parent = models.ForeignKey(FirstLevel)
def breadcrumb(self):
return '{0} / {1}'.format(
self.parent.name,
self.name
)
class ThirdLevel(models.Model):
"""Le troisième niveau.
La modélisation est complètement identique au deuxième niveau;
Juste que la ForeignKey pointe vers une classe différente.
"""
name = models.CharField(max_length=50)
parent = models.ForeignKey(SecondLevel)
def breadcrumb(self):
return '{0} / {1} / {2}'.format(
self.parent.parent.name,
self.parent.name,
self.name
)
\end{minted}
Avant d'aller plus loin, nous voyons clairement à l'étape suivante, on voit clairement que les champs \texttt{name} et la fonction \texttt{breadcrumb()} sont copiés/collés entre les différentes classes.
Avec l'ORM de Django, il est possible de simplifier cette représentation en utilisant une notion d'héritage, mais il y a à nouveau une contrainte, dans la mesure où une clé étrangère ne peut pas être déclarée au niveau d'une classe abstraite.
Cela reviendrait à ceci, ce qui est un chouia plus élégant que la version précédente, mais pas parfait non plus:
\begin{minted}[tabsize=4]{python}
# simple/models.py
class AbstractNode(models.Model):
class Meta:
abstract = True
name = models.CharField(max_length=50)
def breadcrumb(self):
if getattr('parent') is not None:
return [self,]
class FirstLevel(AbstractNode):
pass
class SecondLevel(AbstractNode):
parent = models.ForeignKey(FirstLevel)
class ThirdLevel(AbstractNode):
parent = models.ForeignKey(SecondLevel)
\end{minted}
\begin{minted}[tabsize=4]{python}
from simple.models import *
l1 = FirstLevel(name='Niveau 1')
l2 = SecondLevel(name='Niveau 2', parent=l1)
l3 = ThirdLevel(name='Niveau 3', parent=l2)
l3.breadcrumb()
\end{minted}
Ce qui nous affichera le résultat suivant: \texttt{'Niveau 1 / Niveau 2 / Niveau 3'}.
C'est facile à mettre en place, \ldots mais pas très flexible.
\textit{A priori}, ces noeuds seront référencés par d'autres entités de votre base de données; les problèmes arriveront lorsqu'il faudra changer un noeud de niveau - c'est-à-dire lorsqu'un noeud de niveau 3 (par exemple) "déménagera" vers le niveau 2: cela pourra avoir des repercutions un peu partout, sans parler du fait que ce noeud ne conservera pas son identifiant.
En termes de cohérences de données, il s'agira donc d'une nouvelle entité à part entière (alors qu'elle aura juste bouger un peu dans la structure).
\subsection{Tables liées}
La représentation d'une arborescence grâce à des tables est la plus simple que nous pourrons trouver: elle consiste à créer une table par niveau devant être représenté.
\begin{minted}{python}
from django.db import models
class Level1(models.Model):
name = models.CharField(max_length=255)
class Level2(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(Level1, null=True, blank=True)
class Level3(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(Level2, null=True, blank=True)
class Level4(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(Level3, null=True, blank=True)
\end{minted}
Cette représentation est réellement simpliste, et même si elle peut répondre rapidement à un besoin, nous nous rendons compte rapidement des limites de ce système:
\begin{enumerate}
\item Il est impossible de \textbf{déplacer} une instance d'un niveau vers un autre: tant que l'on restera au même niveau, il sera possible de modifier le parent, mais pas de changer un objet de niveau.
\item Si nous souhaitons ajouter un nouveau niveau, cela reviendra à ajouter une nouvelle classe (et donc, une nouvelle table).
\item La récupération de données (ie. \textit{Le chemin complet vers une entité}) reviendra à exécuter autant de requêtes qu'il y a de niveaux avant la racine.
\end{enumerate}
Ces points impliquent que l'évolutivité de la solution sera rapidement compromise.
\subsection{Listes adjacentes}
Les *Adjency lists* représentent chaque enregistrement avec une clé vers son parent.
Elles permettent une insertion extrêmement rapide, mais posent de gros problèmes de récupération lorsque la profondeur de la structure est inconnue (puisqu'on doit faire autant de jointures qu'il y a de niveaux... et que ceux-ci sont potentiellement infini...).
Il est possible de passer par un contexte d'exécution SQL, mais cela ne changera intrinsèquement rien au nombre de requêtes qui sera effectué.
En résumé, c'est une bonne solution s'il y a beaucoup plus d'écritures que de lectures ou si la quantité d'enregistrements/de niveaux est relativement limitée.
Dans le cas contraire, oubliez la: les performances vont rapidement se dégrader et les interrogations sur la base seront de plus en plus compliquées, notamment si on cherche à récupérer des noeuds spécifiques (eg. en fonction de leur niveau, d'un de leur ancêtre présent dans l'arborescence, ...).
\begin{minted}[tabsize=4]{python}
# adjency_list/models.py
from django.db import models
class Node(models.Model):
name = models.CharField(max_length=50)
parent = models.ForeignKey('self', null=True)
def breadcrumb_list(self):
if self.parent:
return self.parent.breadcrumb_list() + [self.name]
return [self.name]
def breadcrumb(self):
return ' / '.join(self.breadcrumb_list())
def __str__(self):
return self.name
\end{minted}
\begin{minted}{python}
from adjency_list.models import Node
n1 = Node(name='A')
n2 = Node(name='B', parent=n1)
n3 = Node(name='C', parent=n2)
n3.breadcrumb()
\end{minted}
Le résultat sera identique à l'exercice précédent: \texttt{'A / B / C'}.
Si nous regardons le résultat des requêtes effectuées, cela nous donne ceci:
\begin{minted}{python}
from django.db import connection
print(connection.queries)
[
{
'sql': 'SELECT * FROM "adjency_list_node" WHERE "adjency_list_node"."name" = \'C\'',
'time': '0.000'
},
{
'sql': 'SELECT * FROM "adjency_list_node" WHERE "adjency_list_node"."id" = 2',
'time': '0.000'
},
{
'sql': 'SELECT * FROM "adjency_list_node" WHERE "adjency_list_node"."id" = 1',
'time': '0.000'
}
]
\end{minted}
Pour obtenir l'arborescence de cette structure, on voit bien que l'ORM de Django exécute trois requêtes:
\begin{itemize}
\item La première pour récupérer l'objet dont le nom est `C` (celui que l'on cherche),
\item Puis son parent (sur base de la propriété `parent = 2`),
\item Puis le "parent du parent" (sur base de la propriété `parent = 1`).
\end{itemize}
Imaginez maintenant que vous récupériez une arborescence complète sur six niveaux max avec plusieurs centaines de noeuds...
L'avantage de cette présentation est que l'écriture (ajout ou modification) est extrêmement rapide: il suffit de modifier la valeur de la propriété `parent` d'une instance pour que l'arborescence soit modifiée.
L'intégrité de la base de données est constamment conservée.
\subsection{\textit{Path enumeration}}
L'énumération du chemin consiste à ajouter une colonne dans laquelle nous conservons le chemin complet (par exemple `ancêtre1 / ancêtre2 / enfant`).
Dans une certaine mesure, cela revient à stocker toutes les relations avec un noeud dans un champ textuel.
Et c'est extrêmement compliqué à maintenir, car:
\begin{itemize}
\item Si un noeud est modifié, il faut modifier tous les champs qui y font référence
\item Il n'existe aucune aide relative aux méthodes d'insertion/mise à jour/suppression
\item Le caractère d'échappement doit être unique.
\end{itemize}
Rien ne garantit l'intégrité relationnelle de la base de données: si un petit comique modifie la base sans passer par l'API, toute cohérence sera perdue.
\subsection{\textit{Nested sets}}
Les \textit{nested sets} ont pour but de simplifier la gestion présentée ci-dessous, en ajoutant un niveau de complexité sur la modélisation: la lecture de toute la hiérarchie peut se faire en une seule requête, mais l'écriture (ajout, modification et suppression) reste compliquée à implémenter.
Un autre problème est qu'on perd une partie de la cohérence de la base de données: tant que le processus de mise à jour n'est pas terminée, la base peut se trouver dans un état de *Schrödinger*.
L'implémentation consiste à ajouter des valeurs `gauche` et `droite` sur chaque noeud.
L'attribution des valeurs se fait selon un parcours préfixe: pour chaque enregistrement, la valeur `gauche` est plus petite que toutes les valeurs utilisées dans ses descendants; la valeur `droite` est plus grande que celle utilisée par tous ses descendants...
Et ces valeurs sont \textbf{entre} toutes les valeurs gauches/droites de ses ancêtres.
Le problème intervient lorsque vous souhaitez ajouter, modifier ou supprimer un noeud: il vous faut alors recalculer l'ensemble des arborescences, ce qui est loin d'être performant.
En plus de cela, on perd l'intégrité relationnelle, puisqu'un enfant pourrait avoir des valeurs incohérentes: avoir par exemple un parent qui pointe vers un noeud alors que le recalcul des valeurs est en cours.
Il existe des librairies toutes faites pour cela...
Regardez du côté du pattern MPTT si vous ne trouvez rien sur les nested sets.
\subsection{Closure tables}
Le dernier, la *closure* consiste à créer une table supplémentaire reprenant **toutes** les relations d'un noeud vers ses enfants:
\begin{itemize}
\item On doit donc passer par une table connexe pour connaître la structure
\item Cette table peut grandir très rapidement: pour cinq niveaux donnés, le niveau 1 sera lié à tous ses descendants (ainsi qu'à lui-même); le niveau 2 à tous ses descendants; etc. Pour cinq niveaux, on aura donc déjà 5 + 4 + 3 + 2 + 1 entrées.
\end{itemize}
Parmi les avantages, nous conservons l'intégrité relationnelle dans la mesure où chaque enregistrement sera référencé par une clé étrangère.
On a un exemple de remplissage/vidage d'une closure table, mais il faudrait en fait présenter les listes adjacentes et les autres structures de données.
Comme ça on pourra introduire les graphs juste après.
\begin{minted}[tabsize=4]{python}
# <app>/management/commands/rebuild.py
"""This command manages Closure Tables implementation
It adds new levels and cleans links between entities.
This way, it's relatively easy to fetch an entire tree with just one tiny
request.
"""
from django.core.management.base import BaseCommand
from structure.models import Entity, EntityTreePath
class Command(BaseCommand):
def handle(self, *args, **options):
entities = Entity.objects.all()
for entity in entities:
breadcrumb = [node for node in entity.breadcrumb()]
tree = set(EntityTreePath.objects.filter(descendant=entity))
for idx, node in enumerate(breadcrumb):
tree_path, _ = EntityTreePath.objects.get_or_create(
ancestor=node, descendant=entity, weight=idx + 1
)
if tree_path in tree:
tree.remove(tree_path)
for tree_path in tree:
tree_path.delete()
\end{minted}
\section{Graphs}
La représentation de graphs est hors de ce périmètre: il conviendrait d'aborder des bases de données ayant un modèle différent d'un modèle relatonnel, sans quoi les performances seront atrocement complexes et abominables \cite[p. 49-55]{data_intensive}.

106
chapters/urls.tex Executable file
View File

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

30
chapters/views.tex Executable file
View File

@ -0,0 +1,30 @@
\chapter{Vues}
Les vues sont la plaque tournantes entre nos données (\textit{via} le modèle) et le rendus d'un template.
\section{Fonctions}\label{FBV}
Les fonctions
\section{Classes}\label{CBV}
Les \textit{Class Based Views} ne sont jamais que des classes qui encapsulent certains comportements, comme nous l'avons vu en (cf. \#).
La classe présentant le comportement le plus proche d'une fonction est la classe \texttt{View} que l'on trouve dans le paquet \texttt{django.views.generic}:
\begin{minted}{python}
from django.views.generic import View
class WishListView(View):
def get(self):
pass
def post(self):
pass
\end{minted}
Le premier avantage des \textit{CBVs} est de pouvoir isoler les conditions liées au type de méthode HTTP \texttt{if request.method == "GET": ... elif request.method == "POST": ...}
La principale \textit{cheat-sheet} est \href{https://ccbv.co.uk/}{Django Class-Based View inspector}.

616
chapters/working-in-isolation.tex Executable file
View File

@ -0,0 +1,616 @@
\chapter{Travailler en isolation}
Nous allons aborder la gestion et l'isolation des dépendances.
Cette section est aussi utile pour une personne travaillant seule, que pour transmettre les connaissances à un nouveau membre de l'équipe ou pour déployer l'application elle-même.
Il en était déjà question au deuxième point des 12 facteurs: même dans le cas de petits projets, il est déconseillé de s'en passer.
Cela évite les déploiements effectués à l'arrache à grand renfort de \texttt{sudo} et d'installation globale de dépendances, pouvant potentiellement occasioner des conflits entre les applications déployées:
\begin{enumerate}
\item
Il est tout à fait envisagable que deux applications différentes soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance.
\item
Pour la reproductibilité d'un environnement spécifique, cela évite notamment les réponses type "Ca marche chez moi", puisque la construction du nouvel environnement fait partie intégrante du processus de développement et de la documentation du projet; grâce à elle, nous avons la possibilité de construire un environnement sain et d'appliquer des dépendances identiques, quelle que soit l'hôte destiné à accueillir le déploiment.
\end{enumerate}
\begin{figure}
\centering
\includegraphics{images/it-works-on-my-machine.jpg}
\caption{It works on my machine}
\end{figure}
\section{Environnements virtuels}
\begin{figure}
\centering
\includegraphics{images/xkcd-1987.png}
\caption{\url{https://xkcd.com/1987}}
\end{figure}
Un des reproches que l'on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont pas toujours simples ou directes.
Pour quelqu'un qui débarquererait, la quantité d'options différentes peut paraître rebutante.
Nous pensons notamment aux environnements virtuels : ils sont géniaux à utiliser, mais on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version améliorée et plus ergonomique), \texttt{venv} (la version intégrée depuis la version 3.3 de l'interpréteur, et \href{https://docs.python.org/3/library/venv.html}{la manière recommandée} de créer un environnement depuis la 3.5).
Pour créer un nouvel environnement, vous aurez donc besoin:
\begin{enumerate}
\item
D'une installation de Python - \url{https://www.python.org/}
\item
D'un terminal - voir le point \href{../environment/_index.xml\#un-terminal}{Un terminal}
\end{enumerate}
Il existe plusieurs autres modules permettant d'arriver au même résultat, avec quelques avantages et inconvénients pour chacun d'entre eux.
Le plus prometteur d'entre eux est \href{https://python-poetry.org/}{Poetry}, qui dispose d'une interface en ligne de commande plus propre et plus moderne que ce que PIP propose.
\subsection{Poetry}
Poetry se propose de gérer le projet au travers d'un fichier pyproject.toml.
TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à ses heures), se place comme alternative aux formats comme JSON, YAML ou INI.
\begin{verbatim}
$ poetry new django-gecko
$ tree django-gecko/
django-gecko/
django_gecko
__init__.py
pyproject.toml
README.rst
tests
__init__.py
test_django_gecko.py
2 directories, 5 files
\end{verbatim}
Ceci signifie que nous avons directement (et de manière standard):
\begin{itemize}
\item
Un répertoire django-gecko, qui porte le nom de l'application que vous venez de créer
\item
Un répertoires tests, libellé selon les standards de pytest
\item
Un fichier README.rst (qui ne contient encore rien)
\item
Un fichier pyproject.toml, qui contient ceci:
\end{itemize}
\begin{verbatim}
[tool.poetry]
name = "django-gecko"
version = "0.1.0"
description = ""
authors = ["... <...@grimbox.be>"]
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
\end{verbatim}
La commande \texttt{poetry\ init} permet de générer interactivement les fichiers nécessaires à son intégration dans un projet existant.
J'ai pour habitude de conserver mes projets dans un répertoire \texttt{\textasciitilde{}/Sources/} et mes environnements virtuels dans un répertoire \texttt{\textasciitilde{}/.venvs/}.
Cette séparation évite que l'environnement virtuel ne se trouve dans le même répertoire que les sources, ou ne soit accidentellement envoyé vers le système de gestion de versions.
Elle évite également de rendre ce répertoire "visible" - il ne s'agit au fond que d'un paramètre de configuration lié uniquement à votre environnement de développement; les environnements virtuels étant disposables, il n'est pas conseillé de trop les lier au projet qui l'utilise comme base.
Dans la suite de ce chapitre, je considérerai ces mêmes répertoires, mais n'hésitez pas à les modifier.
\begin{dangerbox}
Indépendamment de l'endroit où vous stockerez le répertoire contenant cet environnement, il est primordial de \textbf{ne pas le conserver dans votre dépôt de stockage}.
Cela irait à l'encontre des douze facteurs, cela polluera inutilement vos sources et créera des conflits avec l'environnement des personnes qui souhaiteraient intervenir sur le projet.
\end{dangerbox}
Pur créer notre répertoire de travail et notre environnement virtuel, exécutez les commandes suivantes:
\begin{verbatim}
mkdir ~/.venvs/
python -m venv ~/.venvs/gwift-venv
\end{verbatim}
Ceci aura pour effet de créer un nouveau répertoire (\texttt{\textasciitilde{}/.venvs/gwift-env/}), dans lequel vous trouverez une installation complète de l'interpréteur Python.
Votre environnement virtuel est prêt, il n'y a plus qu'à indiquer que nous souhaitons l'utiliser, grâce à l'une des commandes suivantes:
\begin{verbatim}
# GNU/Linux, macOS
source ~/.venvs/gwift-venv/bin/activate
# MS Windows, avec Cmder
~/.venvs/gwift-venv/Scripts/activate.bat
# Pour les deux
(gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$
\end{verbatim}
A présent que l'environnement est activé, tous les binaires de cet environnement prendront le pas sur les binaires du système.
De la même manière, une variable \texttt{PATH} propre est définie et utilisée, afin que les librairies Python y soient stockées.
C'est donc dans cet environnement virtuel que nous retrouverons le code source de Django, ainsi que des librairies externes pour Python une fois que nous les
aurons installées.
Pour les curieux, un environnement virtuel n'est jamais qu'un répertoire dans lequel se trouve une installation fraîche de l'interpréteur, vers laquelle pointe les liens symboliques des binaires.
Si vous recherchez l'emplacement de l'interpréteur avec la commande \texttt{which\ python}, vous recevrez comme réponse \texttt{/home/fred/.venvs/gwift-env/bin/python}.
Pour sortir de l'environnement virtuel, exécutez la commande \texttt{deactivate}.
Si vous pensez ne plus en avoir besoin, supprimer le dossier.
Si nécessaire, il suffira d'en créer un nouveau.
Pour gérer des versions différentes d'une même librairie, il nous suffit de jongler avec autant d'environnements que nécessaires.
Une application nécessite une version de Django inférieure à la 2.0 ? On crée un environnement, on l'active et on installe ce qu'il faut.
Cette technique fonctionnera autant pour un poste de développement que sur les serveurs destinés à recevoir notre application.
Par la suite, nous considérerons que l'environnement virtuel est toujours activé, même si \texttt{gwift-env} n'est pas indiqué.
a manière recommandée pour la gestion des dépendances consiste à les épingler dans un fichier requirements.txt, placé à la racine du projet.
Ce fichier reprend, ligne par ligne, chaque dépendance et la version nécessaire.
Cet épinglage est cependant relativement basique, dans la mesure où les opérateurs disponibles sont ==, et \textgreater=.
Poetry propose un épinglage basé sur SemVer\index{Semantic Versioning}.
Les contraintes qui peuvent être appliquées aux dépendances sont plus touffues que ce que proposent pip -r, avec la présence du curseur \^{}, qui ne modifiera pas le nombre différent de zéro le plus à gauche:
\begin{verbatim}
^1.2.3 (où le nombre en question est 1) pourra proposer une mise à jour jusqu'à la version juste avant la version 2.0.0
^0.2.3 pourra être mise à jour jusqu'à la version juste avant 0.3.0.
...
\end{verbatim}
L'avantage est donc que l'on spécifie une version majeure - mineure - patchée, et que l'on pourra spécifier accepter toute mise à jour jusqu'à la prochaine version majeure - mineure patchée (non incluse).
Une bonne pratique consiste également, tout comme pour npm, à intégrer le fichier de lock (poetry.lock) dans le dépôt de sources : de cette manière, seules les dépendances testées (et intégrées) seront considérées sur tous les environnements de déploiement.
Il est alors nécessaire de passer par une action manuelle (poetry update) pour mettre à jour le fichier de verrou, et assurer une mise à jour en sécurité (seules les dépendances testées sont prises en compte)
et de qualité (tous les environnements utilisent la même version d'une dépendance).
L'ajout d'une nouvelle dépendance à un projet se réalise grâce à la commande \texttt{poetry\ add\ \textless{}dep\textgreater{}}:
\begin{verbatim}
$ poetry add django
Using version ^3.2.3 for Django
Updating dependencies
Resolving dependencies... (5.1s)
Writing lock file
Package operations: 8 installs, 1 update, 0 removals
• Installing pyparsing (2.4.7)
• Installing attrs (21.2.0)
• Installing more-itertools (8.8.0)
• Installing packaging (20.9)
• Installing pluggy (0.13.1)
• Installing py (1.10.0)
• Installing wcwidth (0.2.5)
• Updating django (3.2 -> 3.2.3)
• Installing pytest (5.4.3)
\end{verbatim}
Elle est ensuite ajoutée à notre fichier \texttt{pyproject.toml}:
\begin{verbatim}
[...]
[tool.poetry.dependencies]
python = "^3.9"
Django = "^3.2.3"
[...]
\end{verbatim}
Et contrairement à \texttt{pip}, pas besoin de savoir s'il faut pointer vers un fichier (\texttt{-r}) ou un dépôt VCS (\texttt{-e}), puisque Poetry va tout essayer, {[}dans un certain ordre{]}(\url{https://python-poetry.org/docs/cli/\#add}).
L'avantage également (et cela m'arrive encore souvent, ce qui fait hurler le runner de Gitlab), c'est qu'il n'est plus nécessaire de penser à épingler la dépendance que l'on vient d'installer parmi les fichiers de
requirements, puisqu'elles s'y ajoutent automatiquement grâce à la commande \texttt{add}.
\subsubsection{Python Packaging Made Easy}
Cette partie dépasse mes compétences et connaissances, dans la mesure où je n'ai jamais rien packagé ni publié sur {[}pypi.org{]}(pypi.org).
Ce n'est pas l'envie qui manque, mais les idées et la nécessité.
Ceci dit, Poetry propose un ensemble de règles et une préconfiguration qui (doivent) énormément facilite(r) la mise à disposition de librairies sur Pypi - et rien que ça, devrait ouvrir une partie de l'écosystème.
Michal Jaworski et Tarek Ziadé en parlent très bien \cite[Chapitres 7 et 8]{expert_python}:
\begin{quote}
Python packaging can be a bit overwhelming at first.
The main reason for that is the confusion about proper tools for creating Python packages.
Anyway, once you create your first package, you will se that this is as hard as it looks.
Also, knowing propre, state-of-the-art packaging helps a lot.
\end{quote}
En gros, c'est ardu-au-début-mais-plus-trop-après.
Et c'est heureusement suivi et documenté par la PyPA (\textbf{\href{https://github.com/pypa}{Python Packaging Authority}}).
Les étapes sont les suivantes:
\begin{enumerate}
\item
Utiliser setuptools pour définir les projets et créer les distributions sources,
\item
Utiliser \textbf{wheels} pour créer les paquets,
\item
Passer par \textbf{twine} pour envoyer ces paquets vers PyPI
\item
Définir un ensemble d'actions (voire, de plugins nécessaires - lien avec le VCS, etc.) dans le fichier \texttt{setup.py}, et définir les propriétés du projet ou de la librairie dans le fichier \texttt{setup.cfg}.
\end{enumerate}
Avec Poetry, deux commandes suffisent \footnote{Théoriquement - puisque j'avoue n'avoir jamais essayé}:
\begin{enumerate}
\item
\texttt{poetry\ build}
\item
\texttt{poetry\ publish}
\end{enumerate}
\begin{verbatim}
$ poetry build
Building geco (0.1.0)
- Building sdist
- Built geco-0.1.0.tar.gz
- Building wheel
- Built geco-0.1.0-py3-none-any.whl
$ tree dist/
dist/
-- geco-0.1.0-py3-none-any.whl
-- geco-0.1.0.tar.gz
0 directories, 2 files
\end{verbatim}
Ce qui est quand même 'achement plus simple que d'appréhender tout un
écosystème.
\section{Un système de virtualisation}
Par "\emph{système de virtualisation}", nous entendons n'importe quel application, système d'exploitation, système de containeurisation, \ldots qui permette de créer ou recréer un environnement de développement aussi proche que celui en production.
Les solutions sont nombreuses:
\begin{itemize}
\item
\href{https://www.virtualbox.org/}{VirtualBox}
\item
\href{https://www.vagrantup.com/}{Vagrant}
\item
\href{https://www.docker.com/}{Docker}
\item
\href{https://linuxcontainers.org/lxc/}{Linux Containers (LXC)}
\item
\href{https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v}{Hyper-V}
\end{itemize}
Ces quelques propositions se situent un cran plus loin que la "simple" isolation d'un environnement, puisqu'elles vous permettront de construire un environnement complet.
Elles constituent donc une étape supplémentaires dans la configuration de votre espace de travail, mais en amélioreront la qualité.
Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux solutions automatisables et multiplateformes, dont la configuration peut faire partie intégrante de vos sources.
\subsection{Vagrant}
\includegraphics{images/logo-vagrant.png}
Vagrant consiste en un outil de création et de gestion d'environnements virtualisés, en respectant toujours une même manière de travailler, indépendamment des choix techniques et de l'infrastructure que vous pourriez sélectionner.
\begin{quote}
Vagrant is a tool for building and managing virtual machine environments
in a single workflow. With an easy-to-use workflow and focus on
automation, Vagrant lowers development environment setup time, increases
production parity, and makes the "works on my machine" excuse a relic of
the past. \footnote{\url{https://www.vagrantup.com/intro}}
\end{quote}
La partie la plus importante de la configuration de Vagrant pour votre projet consiste à placer un fichier \texttt{Vagrantfile} - \emph{a priori} à la racine de votre projet - et qui contiendra les information suivantes:
\begin{itemize}
\item
Le choix du \emph{fournisseur} (\textbf{provider}) de virtualisation (Virtualbox, Hyper-V et Docker sont natifs; il est également possible de passer par VMWare, AWS, etc.)
\item
Une \emph{box}, qui indique le type et la version attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et \href{https://app.vagrantup.com/boxes/search}{il y a du choix}).
\item
La manière dont la fourniture (\textbf{provisioning}) de l'environnement doit être réalisée : scripts Shell, fichiers, Ansible, Puppet, Chef, \ldots
Il est toujours possible de passer par une installation et une maintenance manuelle, après s'être connecté sur la machine.
\item
Si un espace de stockage doit être partagé entre la machine virtuelle et l'hôte
\item
Les ports qui doivent être transmis de la machine virtuelle vers l'hôte.
\end{itemize}
La syntaxe de ce fichier \texttt{Vagrantfile} est en \href{https://www.ruby-lang.org/en/}{Ruby}.
Vous trouverez ci-dessous un exemple, généré (et nettoyé) après avoir exécuté la commande \texttt{vagrant\ init}:
\begin{listing}[H]
\begin{minted}{ruby}
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip:
"127.0.0.1"
config.vm.provider "virtualbox" do |vb|
vb.gui = true
vb.memory = "1024"
end
config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y nginx
SHELL
end
\end{minted}
\end{listing}
Dans le fichier ci-dessus, nous créons une nouvelle machine virtuelle (ie. \emph{invitée}) sous Ubuntu Bionic Beaver, en x64
\begin{itemize}
\item
Avec une correspondance du port \texttt{80} de la machine vers le port \texttt{8080} de l'hôte, en limitant l'accès à celui-ci - accédez à \texttt{localhost:8080} et vous accéderez au port \texttt{80} de la machine virtuelle.
\item
En utilisant Virtualbox comme backend - la mémoire vive allouée sera limitée à 1Go de RAM et nous ne voulons pas voir l'interface graphique au démarrage
\item
Et pour finir, nous voulons appliquer un script de mise à jour \texttt{apt-get\ update} et installer le paquet \texttt{nginx}
\end{itemize}
Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichier \texttt{Vagrantfile} se trouve) sera synchronisé dans le répertoire \texttt{/vagrant} sur la machine invitée.
\subsection{Docker}
\includegraphics{images/docker.jpg}
(copié/collé de cookie-cutter)
\begin{listing}[H]
\begin{verbatim}
version: '3'
volumes:
local_postgres_data: {}
local_postgres_data_backups: {}
services:
<< description des services >>
\end{verbatim}
\end{listing}
\begin{listing}[H]
\begin{verbatim}
django: &django
build:
context: .
dockerfile: ./compose/local/django/Dockerfile
image: khana_local_django
container_name: django
depends_on:
- postgres
volumes:
- .:/app:z
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
ports:
- "8000:8000"
command: /start
\end{verbatim}
\end{listing}
\begin{listing}[H]
\begin{verbatim}
postgres:
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
image: khana_production_postgres
container_name: postgres
volumes:
- local_postgres_data:/var/lib/postgresql/data:Z
- local_postgres_data_backups:/backups:z
env_file:
- ./.envs/.local/.postgres
\end{verbatim}
\end{listing}
\begin{listing}[H]
\begin{verbatim}
docs:
image: khana_local_docs
container_name: docs
build:
context: .
dockerfile: ./compose/local/docs/Dockerfile
env_file:
- ./.envs/.local/.django
volumes:
- ./docs:/docs:z
- ./config:/app/config:z
- ./khana:/app/khana:z
ports:
- "7000:7000"
command: /start-docs
\end{verbatim}
\end{listing}
\begin{listing}[H]
\begin{verbatim}
redis:
image: redis:5.0
container_name: redis
\end{verbatim}
\end{listing}
\begin{listing}[H]
\begin{verbatim}
celeryworker:
<<: *django
image: khana_local_celeryworker
container_name: celeryworker
depends_on:
- redis
- postgres
ports: []
command: /start-celeryworker
\end{verbatim}
\end{listing}
\begin{listing}[H]
\begin{verbatim}
celerybeat:
<<: *django
image: khana_local_celerybeat
container_name: celerybeat
depends_on:
- redis
- postgres
ports: []
command: /start-celerybeat
\end{verbatim}
\end{listing}
\begin{listing}[H]
\begin{verbatim}
flower:
<<: *django
image: khana_local_flower
container_name: flower
ports:
- "5555:5555"
command: /start-flower
\end{verbatim}
\end{listing}
\subsection{Docker-compose}
\begin{listing}[H]
\begin{verbatim}
# docker-compose.yml
version: '3.8'
services:
web:
build: .
command: python /code/manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- 8000:8000
depends_on:
- slqserver
slqserver:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
- "ACCEPT_EULA=Y"
- "SA_PASSWORD=sqklgjqihagrtdgqk12§!"
ports:
- 1433:1433
volumes:
- ../sqlserver/data:/var/opt/mssql/data
- ../sqlserver/log:/var/opt/mssql/log
- ../sqlserver/secrets:/var/opt/mssql/secrets
\end{verbatim}
\end{listing}
\subsection{Dockerfile}
\begin{listing}[H]
\begin{verbatim}
FROM python:3.8-slim-buster
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
RUN apt-get update \
# dependencies for building Python packages
&& apt-get install -y build-essential \
# psycopg2 dependencies
&& apt-get install -y libpq-dev \
# Translations dependencies
&& apt-get install -y gettext \
# cleaning up unused files
&& apt-get purge -y --auto-remove -o
APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*
# Requirements are installed here to ensure they will be cached.
COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt
COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r$//g' /start-celeryworker
RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r$//g' /start-celerybeat
RUN chmod +x /start-celerybeat
COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r$//g' /start-flower
RUN chmod +x /start-flower
WORKDIR /app
ENTRYPOINT ["/entrypoint"]
\end{verbatim}
\end{listing}
\section{Docker \& Dockerfile}
\begin{listing}
\begin{verbatim}
# Dockerfile
# Pull base image
FROM python:3.8-slim-buster
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV DEBIAN_FRONTEND noninteractive
ENV ACCEPT_EULA=Y
# install Microsoft SQL Server requirements.
ENV ACCEPT_EULA=Y
RUN apt-get update -y && apt-get update \
&& apt-get install -y --no-install-recommends curl gcc g++ gnupg
# Add SQL Server ODBC Driver 17
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list
RUN apt-get update \
&& apt-get install -y msodbcsql17 unixodbc-dev
# clean the install.
RUN apt-get -y clean
# Set work directory
WORKDIR /code
# Install dependencies
COPY ./requirements/base.txt /code/requirements/
RUN pip install --upgrade pip
RUN pip install -r ./requirements/base.txt
# Copy project
COPY . /code/
\end{verbatim}
\caption{Un exemple de Dockerfile}
\end{listing}

46
code_samples/shapes.py Executable file
View File

@ -0,0 +1,46 @@
class Shape:
def area(self):
pass
class Square(Shape):
def __init__(self, top_left, side):
self.__top_left = top_left
self.__side = side
def area(self):
return self.__side * self.__side
class Rectangle(Shape):
def __init__(self, top_left, height, width):
self.__top_left = top_left
self.__height = height
self.__width = width
def area(self):
return self.__height * self.__width
class Circle(Shape):
def __init__(self, center, radius):
self.__center = center
self.__radius = radius
def area(self):
PI = 3.141592653589793
return PI * self.__radius**2
class Geometry:
def area(self, shape):
if isinstance(shape, Square):
return shape.side * shape.side
if isinstance(shape, Rectangle):
return shape.height * shape.width
if isinstance(shape, Circle):
raise NoSuchShapeException()

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2021-12-29T10:58:14.673Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.0.2 Chrome/96.0.4664.55 Electron/16.0.5 Safari/537.36" etag="dMCpLJpV5Li7PZHA3tmT" version="16.0.2" type="device"><diagram id="Lilse0xzAZw1GVnBGF5J" name="Page-1">7Zlrb5swFIZ/DeqnRmAIST6mpJdJ7VQp0/bZAQesGpvZJpf9+h1zy4VW6dDaRCpVpcDrY4PP89rYYLlBurmXOEueRESYhexoY7kzCyHHsRH8GGVbKTYalkosaVRpO2FO/5A6sFJzGhF1EKiFYJpmh2IoOCehPtCwlGJ9GLYU7PCqGY5JS5iHmLXVXzTSSamO0WinPxAaJ/WVHX9SlqS4Dq56ohIcifWe5N5abiCF0OVRugkIM9mr81LWu3ujtLkxSbh+TwU9w9n46ddWUVuspGb2T+/7ddXKCrO86rCFfAbt3SzgIDYHAdZW4FrTSSwkBRR1uawDPl6BTi12WpFMva0JSZHziJhO2lC8Tqgm8wyHpnQNpgQt0SmDM6epvSJSk82beXQaOuBrIlKi5RZC6gp+WaN2tFuernfu8L0KeXLgjErElSPjpuUdNDiouP0DQ3Sa4TTCmcaaCq7MHeRmwFBeYk3xOZB+EbO4h2bx2mZx0CtmQfZHmcU9bZZ5SAkPyfUdDY1jLoPqF/GLM5mcNIwz+kzDeKcN81ROJBEtfyH0eom5xkrT31DpIih/Ef+4w0ubcIbvnnDMo4lo0x5fCgnrp949n+yeoXNp7hm33DPLOWklai8NJgEU1u+PeEHYs1C0eIa5s4XQWqQQgBmNjRBCUogEgZnIGxy+xEW+A8GELJp1l8XfXqPTqq4WJvlKS/HSbAtQo+y1YNtj+87Aa1b/5iTCKmmwQklmupFuYrOHGlChRgMK+xk1iESYp3Cb6v/gPV66Ttp4ERoM23zdj8I7aeF9QPeox9sN7+jS8Drt7eUtTGjyykz19zjtB3JH0p4/qN+j1NtQ//y023uLByyL+34GZoCop92Ftn/0wsE+P+n2puBHQsyYlgTrYmxrtdj2vDvxHnuD0dHgHp0feXsdHzCCOUiBiPqJvOOC27u4R7bfXnETBTmmPLbM2v94IzPDGl9/A3Bc0RV5NWSaZQxYVa8fe6N0McrR2s4Znt8po5ZTHg3/OVyckxzo21HxuQCnJuN8obJXd8JTzgnON70xur3wmVzcDNLesz9eYaXgn5rHhRRbzHranWh7znjwiUsDON19nyzK9j7zurd/AQ==</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

9
docker-miktex.sh Executable file
View File

@ -0,0 +1,9 @@
set -e
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments pdflatex main.tex -shell-escape
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments makeindex main.idx
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments bibtex main
# docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments makeglossaries main
docker run -ti -v miktex:/miktex/.miktex -v `pwd`:/miktex/work miktex-pygments pdflatex main.tex -shell-escape
cp main.pdf ~/Nextcloud/
set +e

64
glossary.tex Executable file
View File

@ -0,0 +1,64 @@
\chapter{Glossaire}
\begin{description}
\item[GitHub] est une plateforme commerciale qui permet d'héberger du code source.
Ironiquement, GitHub est une plateforme propriétaire qui héberge des projets open source.
Elle a été lancée en 2008 et est la plus connue/reconnue pour tous les aspects de collaboration sur du code (open) source.
GitHub a aidé à standardiser les pratiques de développement, et a amené les projets open source à un meilleur niveau de visibilité.
\item[http]
\emph{HyperText Transfer Protocol}, ou plus généralement le protocole
utilisé (et détourné) pour tout ce qui touche au \textbf{World Wide
Web}. Il existe beaucoup d'autres protocoles d'échange de données, comme
\href{https://fr.wikipedia.org/wiki/Gopher}{Gopher},
\href{https://fr.wikipedia.org/wiki/File_Transfer_Protocol}{FTP} ou
\href{https://fr.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol}{SMTP}.
\item[IaaS]
\emph{Infrastructure as a Service}, où un tiers vous fournit des
machines (généralement virtuelles) que vous devrez ensuite gérer en bon
père de famille. L'IaaS propose souvent une API, qui vous permet
d'intégrer la durée de vie de chaque machine dans vos flux - en créant,
augmentant, détruisant une machine lorsque cela s'avère nécessaire.
\item[MVC]
Le modèle \emph{Model-View-Controler} est un patron de conception
autorisant un faible couplage entre la gestion des données (le
\emph{Modèle}), l'affichage et le traitement de celles (la \emph{Vue})
et la glue entre ces deux composants (au travers du \emph{Contrôleur}).
\href{https://en.wikipedia.org/wiki/Model\%E2\%80\%93view\%E2\%80\%93controller}{Wikipédia}
\item[ORM]
\emph{Object Relational Mapper}, où une instance est directement (ou à
proximité) liée à un mode de persistance de données.
\item[FOSS, FLOSS, OSS] est un acronyme pour \textit{Free and Open Source Software}, \textit{Free, Libre and Open Source Software} ou (simplement) \textit{Open Source Software}. La contribution a un projet \textit{Open source} n'est pas uniquement au travers de développement, mais également grâce à la correction de bogues, à l'ajout de traductions ou à la gestion de documentation.
\item[PaaS]
\emph{Platform as a Service}, qui consiste à proposer les composants
d'une plateforme (Redis, PostgreSQL, \ldots\hspace{0pt}) en libre
service et disponibles à la demande (quoiqu'après avoir communiqué son
numéro de carte de crédit\ldots\hspace{0pt}).
\item[POO]
La \emph{Programmation Orientée Objet} est un paradigme de programmation informatique.
Elle consiste en la définition et l'interaction de briques logicielles appelées objets ; un objet représente un concept, une idée ou toute entité du monde physique, comme une voiture, une personne ou
encore une page d'un livre.
Il possède une structure interne et un comportement, et il sait interagir avec ses pairs.
Il s'agit donc dereprésenter ces objets et leurs relations ; l'interaction entre les objets via leurs relations permet de concevoir et réaliser les
fonctionnalités attendues, de mieux résoudre le ou les problèmes.
Dès lors, l'étape de modélisation revêt une importance majeure et nécessaire pour la POO.
C'est elle qui permet de transcrire les éléments du réel sous forme virtuelle.
\href{https://fr.wikipedia.org/wiki/Programmation_orient\%C3\%A9e_objet}{Wikipédia}
\item[PyPA] La \emph{Python Package Authority} est une autorité de travail qui se concentre spécifiquement sur la définition de meilleurs standards autour de l'empaquetemment de librairies tierces.
\item[S3]
Amazon \emph{Simple Storage Service} consiste en un système
d'hébergement de fichiers, quels qu'ils soient. Il peut s'agir de
fichiers de logs, de données applications, de fichiers média envoyés par
vos utilisateurs, de vidéos et images ou de données de sauvegardes.
\textbf{\url{https://aws.amazon.com/fr/s3/}.}
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/amazon-s3-arch.png}}
\end{figure}
\end{description}

78
gwiftemplate.sty Executable file
View File

@ -0,0 +1,78 @@
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
% Package definition
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
\ProvidesPackage{gwiftemplate}
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
% Required Package
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
\RequirePackage{xcolor}
\RequirePackage{tcolorbox}
% Color definition
\definecolor{ffg_darkblue}{RGB}{21,29,76}
\definecolor{ffg_lightblue}{RGB}{0, 120, 190}
\definecolor{ffg_red}{RGB}{241, 83, 60}
\definecolor{ffg_title_color}{RGB}{12, 97, 158}
\definecolor{ffg_section_color}{RGB}{24, 146, 239}
\definecolor{footer_color}{RGB}{128, 128, 128}
\definecolor{flyingblue}{RGB}{49, 123, 181}
\definecolor{greymoreknowledge}{RGB}{200, 200, 200}
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
% Define colorbox
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
\newtcolorbox{knowledgebox}{colback=ffg_red!5!white, colframe=ffg_red!100!black, fonttitle=\bfseries, title={Savoir}, breakable} %breakable
\newtcolorbox{skillsbox}{fonttitle=\bfseries, title={Savoir faire}, breakable} %breakable
\newenvironment{advicebox}
{
\begin{tcolorbox}[colback=flyingblue!5!white, colframe=flyingblue!100!black,title={Conseils}]
}
{
\end{tcolorbox}
}
\newenvironment{memorizebox}
{
\begin{tcolorbox}[colback=greymoreknowledge!5!white, colframe=greymoreknowledge!100!black,title={À retenir}]
}
{
\end{tcolorbox}
}
\newenvironment{dangerbox}
{
\begin{tcolorbox}[colback=red!5!white,colframe=red!75!black,title={Important}]
}
{
\end{tcolorbox}
}
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
% Define image boxes
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %
\newenvironment{graphic}[1]
%
% I actually tried to make a `newcommand` with two parameters
% instead of a new environnement but captions were not taken back into the correct format.
%
{
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{#1}}
}
{
\end{figure}
}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% References
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% https://sources.grimbox.be/Sulley/layout_syllabus_ffg/src/branch/master/ffgTemplate.sty
% Définir une nouvelle commande : https://www.overleaf.com/learn/latex/Commands
% Check empty values : https://latex.org/forum/viewtopic.php?t=5976

BIN
ideas/21-10-01 21-30-54 1890.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
ideas/21-10-01 21-31-20 1891.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

0
ideas/libs.rst Normal file → Executable file
View File

0
ideas/resources.md Normal file → Executable file
View File

0
ideas/tests.asciidoc Normal file → Executable file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/12factors/release.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
images/amazon-s3-arch.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
images/arch-comp-modules.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

1
images/arch-structure Executable file
View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-05-23T10:59:53.794Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.5.1 Chrome/96.0.4664.110 Electron/16.0.7 Safari/537.36" etag="Ck-ryfskyNGh9GZ3a6UR" version="16.5.1" type="device"><diagram id="uHdZmcEhaC1admDXuta1" name="Page-1">zZdNb6MwEIZ/DdpTJcCEkGOafkpbrXYjdXurLDwNVg1GZlzI/vo1wYQgt1V7aOGC7HfGH/PYjG2PbPLmWtEyu5MMhBf6rPHIhReGCVmYbyvsrRAnnbBTnHVSMAhb/g+s6FtVcwbVyBGlFMjLsZjKooAURxpVStZjtycpxqOWdAeOsE2pcNW/nGFmowiXg34DfJf1IwfxqrPktHe2kVQZZbI+kcilRzZKSuxKebMB0bLruXTtrt6wHiemoMCPNCjWv5T27x+SR33z+4zK2+XP9Vlk43ihQtuI7Wxx3yNQUhcM2l58j5zXGUfYljRtrbVZc6NlmAtTC0zRdgcKoXlzosExfLNtQOaAam9cbIPYArM7Zmmr9YA/iKyWnaAnvSO1S7479jxQMQUL5jOQEgfSuiwFTylyWVTvAAu+BdhyDCx5BVj4GrAv47VyeF2Y39PbEG+9gtnxOsKZDNjCd4D90QXyHGbHKp6cVeCwuuOMCahNTHPDFfqT4wodXNt9hYd/MWn3l89+QFMKyfGQzWZHMJqcIHEI3nOFmgpezRPZ5AfAInI3HagX0Gp2yZ9Mn/wXLiyU6XN7MZ0brOmzf+yelN29ogKq58Yr+sL0b6rD2+FgO3mAkcv/</diagram></mxfile>

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

BIN
images/commistrip/versions.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-02-21T17:54:02.057Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.5.1 Chrome/96.0.4664.110 Electron/16.0.7 Safari/537.36" etag="-JaFVMhHEClFmUan4B2S" version="16.5.1" type="device"><diagram id="60iCHSiT1cAfRgwXAf6L" name="Page-1">7VfRbtowFP0aHjcRQkt5LIF1mqg0jWmT9lKZ5JK4OL6RcwOkX7/rxCEEBmpVob1UQsg+vo59zzk5gp4fpLsHI7LkESNQvUE/2vX8aW8wGA7H/G2BsgZGw34NxEZGNXQALOQL1KDXoIWMIHdYDRGiIpl1wRC1hpA6mDAGt92yFaqoA2Qihs41LLAIhYKTst8yoqRG7wajFv8KMk6ak71b1/BShOvYYKHdeRo11CupaB7jjswTEeH2APJnPT8wiFSP0l0AyrLaZezLmdX9lQ1oes2Gkmb3P9fPhd58W/95KaaL6OHXp0aAjVCF48LdlsqGHIiYKzdFQwnGqIWateikIgDsOX2etTVzxIxBj8FnICqd8KIgZCihVLnVfA0UJm5SX8CeerZTB+VYmBAutee8JEwMdKHubq8HOxwwBTIl7zOgBMlN9x7CeS3e17Wk88Dx/hYNTiSYIK5PZMi3MlVCW75XqKlRxDImlIw1j0MmCAwDGzAk2d73biGVUVQpFSZSRXNRYmHpyInt28wmCRr5wk8We1WYN3KaDcadioXd6fQ2kHPN90Ye7wh6FLtO4Vzk5IAQlRJZLpdVJxZJWSmpJ0iEqYPO2aRrOsch9w27y7Y5ldltaF5Ul2EcYvV82yaC1+RacpAG4/6VjOH/492c1Jv4rv69HXJrg1tl5VsaHsVUcWGrSBLT2hTmZM4VhoIgRlM+HT/3yIDMLB3pQQbXEKBCNt20yj42p1TqCGr8qWBFl9yZZyKUOp5XZdNhi/xwbFsIeftKVUma8EbQlRdIkGhtlCE3UMlxM+EPCxT0P9/0briNgOdeO+ePLTcUoOZ2hKy8AuzRLVifvttz51/5UyM2zhu/zni310qkuxPjBc4jH6n0n1PJG/VfZ47RtVJp/K5U0iI9CqWPlLlWypz5VfPGlBm+PWV42v6urdYO/jb4s78=</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-02-21T17:57:51.400Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.5.1 Chrome/96.0.4664.110 Electron/16.0.7 Safari/537.36" etag="stiXMf6NY2XbX0NICuCm" version="16.5.1" type="device"><diagram id="60iCHSiT1cAfRgwXAf6L" name="Page-1">7VjbjtowEP0aHluRBFh4XALdqmKlqlS9vVQmMYkXJ4OcCZD9+k4SmxCyULYsqA9ICNnH48ucOTM2tBw32jwotgwfweeyZbf9TcsZtWy70xnQdw5kJXDXaZdAoIRfQjvAVDzzErQMmgqfJxorIQSQKJZ10IM45h7WMKYUrOtmc5B+DViygDeAqcdkE/0ufAxLtG/fVfhHLoLQ7Gz1tMMz5i0CBWms94sh5uVIxMwy2sckZD6sdyBn3HJcBYBlK9q4XOas1hn7cGB0e2TFYzxlQobj+6+LpzRefVr8ek5HU//h2zu9yorJVFMxBFjoA2Nm+EnWIpKMXHOGc4hxqkcs6jMpgpjaHh2DKwJWXKEgau/1QCR8P7ceeqGQ/oRlkObnTZCoM71hCEo808pM6mVpWKEWij2oWUzzmQS3CVU8IZvPhgRrD3pkm5rhhCWoAQ+kZMtEzApPciRiKhDxEBAh0lCI0fZAC45eaPbII859baU5JL/55mBwrG3IKYk4RBxVRiZmQqecofPH7mt9rSs1Wianwh0lGh0ynQDBduVKCdTQYniFMJyGMFr2sJxEOnPu8ya5ZvdkHr6ZolaABRe5FQokWo1hgqqhKKIK9whGBQvuggRS0ahIJFKbkHIPMoKTfI7H5JYsmSfiYFKYjToV8kXTl0NA0+eySMuQJvK4CC4yZJUulkCeFvx2h/ShKLjt991Wl9xwqW9Vffrk5gpdiMkdJorgcxLdmufCO1tEh3O4qSwjpROVZLA3V1K/oSSXIQ9AL30rM1csM3a7W6szltU/TR13l1LH4Kw6E7PoVmauVGb6b1NmrAsJyeq+oKQ9NXCfXn26S/SFEEDM5LhC68xVJhOApQafOGKmKwZLEerqKrfM9zmeoXQsSJXHjzlkHsJUL/ixwAxeDozikqFY1Q/y9rT3/gfaiVyV/cglThmiuz+14ovOaFPrZbVsuG64DlToK4Wr+eCf0YP/t3e7kv/lSt69N1q2M+973PMalwyNzPrdTvcyvxUsk3F/K72DS93hln3WJV4I8DRTI9OG+Suv/d2IHIrh7QHwYu04+wXQe31to271P0kxtvM3lDP+Aw==</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Some files were not shown because too many files have changed in this diff Show More