Move tests, create project structure, ...
This commit is contained in:
parent
1a8b37b2a6
commit
df11e89a61
|
@ -45,7 +45,7 @@ $ poetry new django-gecko
|
|||
$ django-gecko
|
||||
├── README.md
|
||||
├── django_gecko
|
||||
│ └── __init__.py
|
||||
│ └── __init__.py
|
||||
├── pyproject.toml
|
||||
└── tests
|
||||
└── __init__.py
|
||||
|
|
|
@ -1 +1,75 @@
|
|||
== Différentes structures de projets
|
||||
== Différentes structures de projets
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire `wish` dans votre répertoire `gwift` existant. C’est _une_ forme de
|
||||
convention.
|
||||
|
||||
Il manque quelques fichiers utiles, qui seront décrits par la suite, pour qu’une application soit réellement autonome: templates, `urls.py`, managers, services, ...
|
||||
|
||||
=== Structure d'une application
|
||||
|
||||
. fichier `urls.py` pour définir son propre contexte d'appel,
|
||||
|
||||
==== Structure finale
|
||||
|
||||
En repartant de la structure initiale décrite au chapitre précédent,
|
||||
nous arrivons à ceci.
|
||||
|
||||
TODO : passer à poetry
|
||||
|
||||
==== 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, ...
|
||||
|
||||
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
|
||||
https://cookiecutter.readthedocs.io/[Cookie-Cutter], qui se base sur des
|
||||
canevas _type 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 https://cookiecutter-django.readthedocs.io[ceux qui
|
||||
existent déjà].
|
||||
|
||||
Pour démarrer, créez un environnement virtuel (comme d’habitude):
|
||||
|
||||
....
|
||||
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!
|
||||
....
|
||||
|
||||
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 `--template`, suivie d’un
|
||||
argument reprenant le nom de votre projet (`<my_project>`), lors de
|
||||
l’initialisation d’un projet avec la commande `startproject` de
|
||||
`django-admin`, afin de calquer votre arborescence sur un projet
|
||||
existant. La
|
||||
https://docs.djangoproject.com/en/stable/ref/django-admin/#startproject[documentation]
|
||||
à ce sujet est assez complète.
|
||||
|
||||
....
|
||||
django-admin.py startproject --template=https://[...].zip <my_project>
|
||||
....
|
||||
|
|
|
@ -57,7 +57,7 @@ Les composants périphériques gravitent autour du fonctionnement normal de l’
|
|||
____
|
||||
"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 includedfail 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…it usually does, right ?").
|
||||
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...it usually does, right ?").
|
||||
Only after six hours into the outage did they activate any business continuity procedures.
|
||||
____
|
||||
|
||||
|
@ -82,8 +82,8 @@ temps que prennent chacune d’entre elles |HAProxy
|
|||
Si nous schématisons l’infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante :
|
||||
|
||||
. L’utilisateur fait une requête via son navigateur (Firefox ou Chrome)
|
||||
. Le navigateur envoie une requête http, sa version, un verbe (GET, POST, …), un port et éventuellement du contenu
|
||||
. Le firewall du serveur (Debian GNU/Linux, CentOS, …vérifie si la requête peut être prise en compte
|
||||
. Le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
|
||||
. Le firewall du serveur (Debian GNU/Linux, CentOS, ...vérifie si la requête peut être prise en compte
|
||||
. La requête est transmise à l’application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
|
||||
. Elle est ensuite transmise par socket et est prise en compte par un
|
||||
des _workers_ (= un processus Python) instancié par Gunicorn. Sil’un de
|
||||
|
@ -108,7 +108,7 @@ Les répartiteurs de charges sont super utiles pour donner du mou à l’infrast
|
|||
|
||||
. Maintenance et application de patches,
|
||||
. Répartition des utilisateurs connectés,
|
||||
. …
|
||||
. ...
|
||||
|
||||
==== Serveurs d’application (_Workers_)
|
||||
|
||||
|
@ -167,21 +167,21 @@ ____
|
|||
Chaque évènement est associé à un niveau; celui-ci indique sa criticité et sa émet un premier tri quant à sa pertinence.
|
||||
|
||||
* *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.
|
||||
* *INFO*: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, …
|
||||
* *INFO*: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, ...
|
||||
* *WARN*: Regroupe les informations qui pourraient potentiellement devenir des erreurs.
|
||||
* *ERROR*: Indique les informations internes - Erreur lors de l’appel d’une API, erreur interne, …
|
||||
* *FATAL* (ou *EXCEPTION*): …généralement suivie d’une terminaison du programme - Bind raté d’un socket, etc.
|
||||
* *ERROR*: Indique les informations internes - Erreur lors de l’appel d’une API, erreur interne, ...
|
||||
* *FATAL* (ou *EXCEPTION*): ...généralement suivie d’une terminaison du programme - Bind raté d’un socket, etc.
|
||||
|
||||
La configuration des _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 (_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.
|
||||
Iln’est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les `+StreamHandler+`, qui seront pris en charge par gunicorn .
|
||||
Iln’est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les `StreamHandler`, qui seront pris en charge par gunicorn .
|
||||
La configuration que nous allons utiliser est celle-ci :
|
||||
|
||||
. *Formattage*: à définir - mais la variante suivante est complète, lisible et pratique:
|
||||
`+{levelname} {asctime} {module} {process:d} {thread:d} {message}+`
|
||||
. *Handler*: juste un, qui définit un `+StreamHandler+`
|
||||
. *Logger*: pour celui-ci, nous avons besoin d’un niveau (`+level+`) et de savoir s’il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de `+propagate+` à `+True+`.
|
||||
`{levelname} {asctime} {module} {process:d} {thread:d} {message}`
|
||||
. *Handler*: juste un, qui définit un `StreamHandler`
|
||||
. *Logger*: pour celui-ci, nous avons besoin d’un niveau (`level`) et de savoir s’il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de `propagate` à `True`.
|
||||
|
||||
Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant :
|
||||
|
||||
|
@ -202,11 +202,11 @@ Des erreurs sur un environnement de production arriveront, tôt ou tard, et sero
|
|||
Comme nous l’avons vu, en fonction de l’infrastructure choisie, il existe plusieurs types de journaux:
|
||||
|
||||
. Les journaux applicatifs: ie. le flux d’évènements généré par votre application Django
|
||||
. Les journaux du serveur: Nginx, Gunicorn, …
|
||||
. Les journaux du serveur: Nginx, Gunicorn, ...
|
||||
. Les journaux des autres composants: base de données, service de mise en cache, ...
|
||||
|
||||
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, …, il vous sera physiquement impossible de récupérer une information cohérente.
|
||||
En pratique, c’est impossible : entre les répartiteurs de charge, les différents serveurs, ..., 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:
|
||||
|
||||
image:infrastructure/mattsegal-logging.png[[align="center"]]
|
||||
|
@ -224,13 +224,13 @@ La première étape consiste à agréger ces données dans un dépôt centralis
|
|||
|
||||
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, …
|
||||
disques systèmes, l’utilisation du réseau, ...
|
||||
|
||||
. *Métier*: Le nombre de ventes réalisées, le nombre de nouveaux utilisateurs, les résultats de tests A/B, …
|
||||
. *Application*: Le délai de réalisation d’une transaction, le temps de réponse par utilisateur, …
|
||||
. *Infrastructure*: Le trafic du serveur Web, le taux d’occupation du CPU, …
|
||||
. *Côté client*: Les erreurs applicatives, les transactions côté utilisateur, …
|
||||
. *Pipeline de déploiement*: Statuts des builds, temps de mise à disposition d’une fonctionnalité, fréquence des déploiements, statuts des environnements, …
|
||||
. *Métier*: Le nombre de ventes réalisées, le nombre de nouveaux utilisateurs, les résultats de tests A/B, ...
|
||||
. *Application*: Le délai de réalisation d’une transaction, le temps de réponse par utilisateur, ...
|
||||
. *Infrastructure*: Le trafic du serveur Web, le taux d’occupation du CPU, ...
|
||||
. *Côté client*: Les erreurs applicatives, les transactions côté utilisateur, ...
|
||||
. *Pipeline de déploiement*: Statuts des builds, temps de mise à disposition d’une fonctionnalité, fréquence des déploiements, statuts des environnements, ...
|
||||
|
||||
Bien utilisés, ces outils permettent de prévenir des incidents de manière empirique.
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
== Administration auto-générée
|
|
@ -208,7 +208,7 @@ class Bidule(models.Model):
|
|||
raise ValidationError('Title does not start with T')
|
||||
----
|
||||
|
||||
Il n’est plus nécessaire de définir d’attribut `validators=[…]`, puisque Django va appliquer un peu d’introspection pour récupérer toutes les méthodes qui commencent par `clean_` et pour les faire correspondre au nom du champ à valider (ici, `title`).
|
||||
Il n’est plus nécessaire de définir d’attribut `validators=[...]`, puisque Django va appliquer un peu d’introspection pour récupérer toutes les méthodes qui commencent par `clean_` et pour les faire correspondre au nom du champ à valider (ici, `title`).
|
||||
|
||||
==== Clean
|
||||
|
||||
|
@ -282,7 +282,7 @@ Nous obtenons ainsi une structure représentée par un dictionnaire, qui décrit
|
|||
=== Dépendance avec le modèle
|
||||
|
||||
Un *form* peut hériter d’une autre classe Django.
|
||||
Pour cela, il suffit de fixer l’attribut `model` au niveau de la `class Meta` dans la définition.
|
||||
Pour cela, il suffit de fixer l’attribut `model` au niveau de la `class Meta` dans la définition.
|
||||
|
||||
[source,python]
|
||||
----
|
||||
|
@ -298,7 +298,7 @@ Pour cela, il suffit de fixer l’attribut `model` au niveau de la `class Meta`
|
|||
----
|
||||
|
||||
Notre form dépendra automatiquement des champs déjà déclarés dans la classe `Wishlist`.
|
||||
Cela suit le principe de DRY <don’t repeat yourself et évite qu’une modification ne pourrisse le code: en testant les deux champs présent dans l’attribut fields, nous pourrons nous assurer de faire évoluer le formulaire en fonction du modèle sur lequel il se base.
|
||||
Cela suit le principe de DRY <don’t repeat yourself et évite qu’une modification ne pourrisse le code: en testant les deux champs présent dans l’attribut fields, nous pourrons nous assurer de faire évoluer le formulaire en fonction du modèle sur lequel il se base.
|
||||
|
||||
|
||||
=== Rendu et affichage
|
||||
|
@ -323,7 +323,7 @@ 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, …
|
||||
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, ...
|
||||
Tout ça demande énormément de temps. Et c’est là qu’intervient 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 *layout* et la présentation.
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ La définition d'un ordre par défaut au niveau du modèle ne signifie pas qu’
|
|||
Il est donc nécessaire de tester ce type de comportement _dans son ensemble_, dans la mesure où un `.first()` dans votre code (l'équivalent du `TOP 1` en SQL) pourrait être modifié simplement par cette petite information.
|
||||
====
|
||||
|
||||
TIP: Pour sélectionner un objet au pif: `return Category.objects.order_by("?").first()`
|
||||
TIP: Pour sélectionner un objet au pif: `return Category.objects.order_by("?").first()`
|
||||
|
||||
|
||||
=== Contraintes
|
||||
|
|
|
@ -198,7 +198,7 @@ class Migration(migrations.Migration):
|
|||
]
|
||||
----
|
||||
|
||||
* La migration supprime l’ancienne clé étrangère …
|
||||
* La migration supprime l’ancienne clé étrangère ...
|
||||
* ... et ajoute une nouvelle table, permettant de lier nos catégories à nos
|
||||
livres.
|
||||
|
||||
|
@ -301,7 +301,7 @@ TODO intégrer ici un point sur les updates db - voir designing data-intensive a
|
|||
|
||||
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, _squashées_, … et feront partie intégrante du processus de mise à jour de l’application.
|
||||
peuvent être retravaillées, _squashées_, ... 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é `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
|
||||
|
@ -417,7 +417,7 @@ class Migration(migrations.Migration):
|
|||
]
|
||||
----
|
||||
|
||||
La commande `python manage.py squashmigrations library 0002 0003` appliquera une fusion entre les migrations numérotées `0002` et `0003`:
|
||||
La commande `python manage.py squashmigrations library 0002 0003` appliquera une fusion entre les migrations numérotées `0002` et `0003`:
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
|
@ -491,4 +491,4 @@ En résumé:
|
|||
`__init__.py`)
|
||||
. Soit on réinitialise proprement les migrations avec un `–fake-initial`
|
||||
(sous réserve que toutes les personnes qui utilisent déjà le projet s’y
|
||||
conforment…Ce qui n’est pas gagné.
|
||||
conforment...Ce qui n’est pas gagné.
|
||||
|
|
|
@ -631,7 +631,7 @@ Cela nous dit plus ou moins de quoi il s'agit (_C'est un livre, et il porte l'id
|
|||
==== Nomenclature des relations
|
||||
|
||||
epuis le code, à partir de l’instance de la classe `Item`, on peut donc accéder à la liste en appelant la propriété `wishlist` de notre instance.
|
||||
*A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `<nom de la propriété>_set`; ici `item_set`.
|
||||
*A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `<nom de la propriété>_set`; ici `item_set`.
|
||||
|
||||
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l’attribut `related_name` afin de nommer la relation inverse.
|
||||
|
||||
|
@ -740,7 +740,7 @@ Nous pensons ici à un UUID ou à un slug , qui permettrait d’avoir une sorte
|
|||
|
||||
==== 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 `abstract = True`, on se retrouvera en fait avec quatre tables SQL:
|
||||
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 `abstract = True`, on se retrouvera en fait avec quatre tables SQL:
|
||||
|
||||
* Une table `AbstractModel`, qui reprend les deux champs `created_at` et
|
||||
`updated_at`
|
||||
|
|
|
@ -103,8 +103,8 @@ souhaits dont le nom contient (avec une casse insensible) la chaîne
|
|||
|
||||
Pour un ’OR’, on a deux options :
|
||||
|
||||
. Soit passer par deux querysets, typiuqment `queryset1 queryset2`
|
||||
. Soit passer par des `Q objects`, que l’on trouve dans le namespace
|
||||
. Soit passer par deux querysets, typiuqment `queryset1 queryset2`
|
||||
. Soit passer par des `Q objects`, que l’on trouve dans le namespace
|
||||
`django.db.models`.
|
||||
|
||||
[source,python]
|
||||
|
|
|
@ -189,14 +189,14 @@ Le fonctionnement de la méthode `.prefetch_related()` est identique à la méth
|
|||
|
||||
[WARNING]
|
||||
====
|
||||
Il faut faire attention au prefetch related, qui fonctionne grâce à une grosse requête dans laquelle nous trouvons un `IN (...)`.
|
||||
Il faut faire attention au prefetch related, qui fonctionne grâce à une grosse requête dans laquelle nous trouvons un `IN (...)`.
|
||||
Dans cette optique,
|
||||
|
||||
1. Django récupère tous les objets demandés,
|
||||
2. Pour ensuite prendre toutes les clés primaires,
|
||||
3. 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), cette seconde requête pourrait ne pas fonctionner et vous obtiendrez une exception de type `django.db.utils.OperationalError: too many SQL variables`, qu'il conviendra de gérer correctement.
|
||||
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), cette seconde requête pourrait ne pas fonctionner et vous obtiendrez une exception de type `django.db.utils.OperationalError: too many SQL variables`, qu'il conviendra de gérer correctement.
|
||||
|
||||
Nous pourrions penser qu’utiliser un itérateur permettrait de combiner les deux, mais ce n’est pas le cas.
|
||||
Comme l’indique la documentation:
|
||||
|
|
|
@ -14,15 +14,9 @@ Si vous manquez d’idées ou si vous ne savez pas par où commencer :
|
|||
* https://code.visualstudio.com/[Visual Studio Code] ou https://vscodium.com/[VSCodium], avec le greffon
|
||||
https://marketplace.visualstudio.com/items?itemName=ms-python.python[Python]
|
||||
* https://www.jetbrains.com/pycharm/[PyCharm]
|
||||
* https://www.vim.org/[Vim] avec les greffons
|
||||
https://github.com/davidhalter/jedi-vim[Jedi-Vim] et
|
||||
https://github.com/preservim/nerdtree[nerdtree].
|
||||
* https://www.vim.org/[Vim] avec les greffons https://github.com/davidhalter/jedi-vim[Jedi-Vim] et https://github.com/preservim/nerdtree[nerdtree].
|
||||
|
||||
Si vous hésitez, et même si Codium n’est pas le plus léger (la faute à
|
||||
https://www.electronjs.org/[Electron]...), 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.
|
||||
Si vous hésitez, et même si Codium n’est pas le plus léger (la faute à https://www.electronjs.org/[Electron]...), 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.
|
||||
|
||||
image::environment/codium.png[align="center"]
|
||||
|
||||
|
@ -149,15 +143,15 @@ normes PCI parce les données des titulaires de cartes ne sont pas isolées corr
|
|||
|
||||
Il suffit alors de:
|
||||
|
||||
. Sauver le travail en cours (`git add . && git commit -m [WIP]`)
|
||||
. Revenir sur la branche principale (`git checkout main`)
|
||||
. Créer un "hotfix" (`git checkout -b hotfix/pci-compliance`)
|
||||
. Sauver le travail en cours (`git add . && git commit -m [WIP]`)
|
||||
. Revenir sur la branche principale (`git checkout main`)
|
||||
. Créer un "hotfix" (`git checkout -b hotfix/pci-compliance`)
|
||||
. Solutionner le problème (sans doute un `;` en trop ?)
|
||||
. Sauver le correctif sur cette branche
|
||||
(`git add . && git commit -m "Did it!"`)
|
||||
(`git add . && git commit -m "Did it!"`)
|
||||
. Récupérer ce correctif sur la branche principal
|
||||
(`git checkout main && git merge hotfix/pci-compliance`)
|
||||
. Et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que l’univers vous réclame à cor et à a cri (`git checkout features/dinolol`)
|
||||
(`git checkout main && git merge hotfix/pci-compliance`)
|
||||
. Et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que l’univers vous réclame à cor et à a cri (`git checkout features/dinolol`)
|
||||
|
||||
Il existe plusieurs méthodes pour gérer ces flux d’informations.
|
||||
Les plus connues sont https://www.gitflow.com/[Gitflow] et
|
||||
|
@ -183,7 +177,7 @@ articles pour débuter avec Git:
|
|||
|
||||
==== Décrire ses changements
|
||||
|
||||
La description d’un changement se fait _via_ la commande `git commit`.
|
||||
La description d’un changement se fait _via_ la commande `git commit`.
|
||||
Il est possible de lui passer directement le message associé à ce
|
||||
changement grâce à l’attribut `-m`, mais c’est une pratique relativement
|
||||
déconseillée: un _commit_ ne doit effectivement pas obligatoirement être
|
||||
|
@ -322,29 +316,63 @@ image::apps/bruno.png[align="center"]
|
|||
|
||||
=== Makefile
|
||||
|
||||
Pour gagner un peu de temps, n’hésitez pas à créer un fichier `+Makefile+` que vous placerez à la racine du projet. L’exemple
|
||||
ci-dessous permettra, grâce à la commande `+make coverage+`, d’arriver au même résultat que ci-dessus:
|
||||
Pour gagner un peu de temps, n’hésitez pas à créer un fichier `Makefile` que vous placerez à la racine du projet. L’exemple
|
||||
ci-dessous permettra, grâce à la commande `make coverage`, d’arriver au même résultat que ci-dessus:
|
||||
|
||||
....
|
||||
# Makefile for gwift
|
||||
#
|
||||
```
|
||||
# Makefile for gwift
|
||||
#
|
||||
|
||||
# User-friendly check for coverage
|
||||
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The 'coverage' command was not found. Make sure you have coverage installed)
|
||||
endif
|
||||
# User-friendly check for coverage
|
||||
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The 'coverage' command was not found. Make sure you have coverage installed)
|
||||
endif
|
||||
|
||||
.PHONY: help coverage
|
||||
.PHONY: help coverage
|
||||
|
||||
help:
|
||||
@echo " coverage to run coverage check of the source files."
|
||||
help:
|
||||
@echo " coverage to run coverage check of the source files."
|
||||
|
||||
coverage:
|
||||
coverage run --source='.' manage.py test; coverage report; coverage html;
|
||||
@echo "Testing of coverage in the sources finished."
|
||||
....
|
||||
coverage:
|
||||
coverage run --source='.' manage.py test; coverage report; coverage html;
|
||||
@echo "Testing of coverage in the sources finished."
|
||||
```
|
||||
|
||||
Pour conclure, `make` peu sembler un peu désuet, mais reste extrêmement efficace.
|
||||
|
||||
=== Licence
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
Choisissez une licence.
|
||||
====
|
||||
|
||||
Si votre projet n'a pas de licence qui lui est associée, 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.
|
||||
Une https://github.com/facebook/react/issues/7293[_issue_] a par exemple été adressée en 2017 sur la librairie _React_, et qui aurait pu créer un conflit de revendication de brevet.
|
||||
|
||||
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.
|
||||
|
||||
Parmi toutes les licences existant, trois licences d'entre elles sont généralement proposées et utilisées :
|
||||
|
||||
. *MIT*
|
||||
. *GPLv3*
|
||||
. *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:[_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 - https://fair.io_]
|
||||
. *WTFPL*
|
||||
|
||||
Mike Perham, qui maintient https://sidekiq.org/[Sidekiq], a ainsi proposé une forme de dualité entre la mise à disposition du code source et son utilisation :
|
||||
____
|
||||
_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._
|
||||
|
||||
_...[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._
|
||||
____
|
||||
|
||||
Pour la petite histoire, `+make+` peu sembler un peu désuet, mais reste extrêmement efficace.
|
||||
|
||||
=== Conclusions
|
||||
|
||||
|
@ -355,11 +383,12 @@ Pour résumer :
|
|||
. Prenez un gestionnaire de mot de passe (vous en aurez besoin par la suite pour gérer vos clés et mots de passe),
|
||||
. Appréhendez Git - c'est le standard actuel dans l'industrie,
|
||||
. Choisissez un moteur de base de données (et si vous ne savez pas lequel, contentez-vous d'SQLite dans un premier temps),
|
||||
. Compétez votre ceinture à outils avec tous ceux que vous jugerez bons de retenir.
|
||||
. Compétez votre ceinture à outils avec tous ceux que vous jugerez bons de retenir,
|
||||
. Choisissez une licence d'utilisation.
|
||||
|
||||
Pour récapituler les différents outils que nous proposons :
|
||||
|
||||
* IDE : VSCode,
|
||||
* IDE : VSCode ou PyCharm,
|
||||
* Langage de programmation : Python,
|
||||
* Suivi des versions : Git,
|
||||
* Automatisation des tâches : Make.
|
||||
|
|
|
@ -298,7 +298,7 @@ parente. Mathématiquement, ce principe peut être défini de la manière
|
|||
suivante:
|
||||
|
||||
____
|
||||
[… if S is a subtype of T, then objects of type T in a computer program
|
||||
[... 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,
|
||||
|
@ -392,7 +392,7 @@ class MarkdownRenderer(XmlRenderer):
|
|||
return markdown.markdown(document.content)
|
||||
----
|
||||
|
||||
Le code ci-dessus ne porte pas immédiatement à conséquence, mais dès que nous invoquerons la méthode `header()` sur une instance de type `MarkdownRenderer`, nous obtiendrons un bloc de déclaration XML (`<?xml version = "1.0"?>`) pour un fichier Markdown, ce qui n’aura aucun sens.
|
||||
Le code ci-dessus ne porte pas immédiatement à conséquence, mais dès que nous invoquerons la méthode `header()` sur une instance de type `MarkdownRenderer`, nous obtiendrons un bloc de déclaration XML (`<?xml version = "1.0"?>`) 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 `header()` qu’au niveau de la classe `XmlRenderer` :
|
||||
|
||||
|
@ -898,7 +898,7 @@ Avec cette approche, les composants seront déjà découplés au mieux.
|
|||
|
||||
Les composants peuvent être découpés au niveau:
|
||||
|
||||
* *Du code source*, via des modules, paquets, dépendances, …
|
||||
* *Du code source*, via des modules, paquets, dépendances, ...
|
||||
* *Du déploiement ou de l’exécution*, au travers de dll, jar, linked libraries, ..., voire au travers de threads ou de processus locaux.
|
||||
* *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.
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ En plus de cela, chaque développeur a une copie de l’application qui fonction
|
|||
====
|
||||
|
||||
Comme l’explique Eran Messeri, ingénieur dans le groupe Google Developer Infrastructure: "_Un des avantages d’utiliser un dépôt unique de sources, est qu’il permet un accès facile et rapide à la forme la plus à jour du code, sans aucun besoin de coordination_ cite:[devops_handbook(288-298)].
|
||||
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, …), outils de déploiement, standards de tests (y compris ce qui touche à la sécurité), outils d’analyse et de monitoring ou tutoriaux.
|
||||
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, ...), outils de déploiement, standards de tests (y compris ce qui touche à la sécurité), outils d’analyse et de monitoring ou tutoriaux.
|
||||
|
||||
[[dependencies]]
|
||||
=== Déclaration explicite et isolation des dépendances
|
||||
|
@ -74,7 +74,7 @@ Lors de la création d’un nouvel environnement vierge, il suffira d’utiliser
|
|||
====
|
||||
|
||||
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; 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.
|
||||
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; 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.].
|
||||
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.
|
||||
|
@ -88,7 +88,7 @@ La majorité des langages modernes proposent des mécanismes similaires :
|
|||
* https://rubygems.org/[Gem] pour Ruby,
|
||||
* https://www.npmjs.com/[NPM] pour NodeJS,
|
||||
* link:Maven[https://maven.apache.org/] pour Java,
|
||||
* …
|
||||
* ...
|
||||
|
||||
=== Configuration applicative
|
||||
|
||||
|
@ -148,7 +148,7 @@ Le risque de se retrouver avec une liste colossale de paramètres n’est cepend
|
|||
|
||||
=== Ressources externes
|
||||
|
||||
Nous parlons de bases de données, de services de mise en cache, d’API externes, …
|
||||
Nous parlons de bases de données, de services de mise en cache, d’API externes, ...
|
||||
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 *ressources attachées*, dont la présence est nécessaire au bon fonctionnement de l’application, mais pour lesquelles le *type* n’est pas obligatoirement défini.
|
||||
|
||||
|
@ -172,7 +172,7 @@ Nous serons ainsi ravis de pouvoir simplement modifier la chaîne de connexion `
|
|||
|
||||
image::12-factors/separate-run-steps.png[align="center"]
|
||||
|
||||
Parmi les solutions possibles, nous pourrions nous baser sur les _releases_ de Gitea, sur un serveur d’artefacts (https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano]), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, …).
|
||||
Parmi les solutions possibles, nous pourrions nous baser sur les _releases_ de Gitea, sur un serveur d’artefacts (https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano]), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, ...).
|
||||
|
||||
Dans le cas de Python, la phase de construction (_Build_) correspondra plutôt à une phase d’_empaquettage_ (_packaging_).
|
||||
Une fois préparée, la librairie ou l’application pourra être publiée sur pypi.org ou un dépôt géré en interne.
|
||||
|
|
|
@ -31,7 +31,8 @@ Les sauts de versions sont bien documentés, et ne nécessitent que quelques heu
|
|||
. Créez un nouveau répertoire de travail (`mkdir generic`)
|
||||
. Déplacez l'invite de commande dans ce nouveau répertoire (`cd generic`)
|
||||
. Initialiser un nouvel espace pour Poetry (`poetry init --no-interaction`)
|
||||
. Ajoutez-y Django comme dépendance (`poetry add django`).
|
||||
. Ajoutez-y Django comme dépendance (`poetry add django`),
|
||||
. Démarrez votre serveur de développement (`poetry run python manage.py runserver`).
|
||||
|
||||
|
||||
```bash
|
||||
|
@ -41,13 +42,16 @@ poetry run django-admin startproject gwift .
|
|||
poetry run python manage.py runserver
|
||||
```
|
||||
|
||||
Ici, la commande `+pip install django+` récupère la *dernière version connue disponible dans les dépôts https://pypi.org/*.
|
||||
[IMPORTANT]
|
||||
====
|
||||
La commande `poetry add django` récupère la *dernière version connue disponible dans les dépôts https://pypi.org/*.
|
||||
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 (cf. <<dependencies>>).
|
||||
====
|
||||
|
||||
L’installation de Django a ajouté un nouvel exécutable: `+django-admin+`, que l’on peut utiliser pour créer notre nouvel espace de travail.
|
||||
Par la suite, nous utiliserons `+manage.py+`, qui constitue un *wrapper* autour de `+django-admin+`.
|
||||
L’installation de Django a ajouté un nouvel exécutable: `django-admin`, que l’on peut utiliser pour créer notre nouvel espace de travail.
|
||||
Par la suite, nous utiliserons plutôt la commande `manage.py`, qui constitue un *wrapper* autour de `django-admin`.
|
||||
|
||||
Pour démarrer notre projet, nous lançons `+django-admin startproject gwift+` au travers de Poetry :
|
||||
Pour démarrer notre projet, nous lançons `poetry run django-admin startproject gwift` au travers de Poetry :
|
||||
|
||||
```bash
|
||||
$ poetry run django-admin startproject gwift . <1>
|
||||
|
@ -63,11 +67,11 @@ $ tree .
|
|||
.
|
||||
├── db.sqlite3
|
||||
├── gwift
|
||||
│ ├── __init__.py
|
||||
│ ├── asgi.py
|
||||
│ ├── settings.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
│ ├── __init__.py
|
||||
│ ├── asgi.py
|
||||
│ ├── settings.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
├── manage.py
|
||||
├── poetry.lock
|
||||
└── pyproject.toml
|
||||
|
@ -78,17 +82,15 @@ Le but est de faire en sorte que toutes les opérations de maintenance, déploie
|
|||
|
||||
L’utilité de ces fichiers est définie ci-dessous :
|
||||
|
||||
* `+settings.py+` contient tous les paramètres globaux à notre projet,
|
||||
* `+urls.py+` contient les variables de routes, les adresses utilisées
|
||||
* `settings.py` contient tous les paramètres globaux à notre projet,
|
||||
* `urls.py` contient les variables de routes, les adresses utilisées
|
||||
et les fonctions vers lesquelles elles pointent,
|
||||
* `+manage.py+`, pour toutes les commandes de gestion,
|
||||
* `+asgi.py+` contient la définition de l’interface https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface[ASGI],
|
||||
* `manage.py`, pour toutes les commandes de gestion,
|
||||
* `asgi.py` contient la définition de l’interface https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface[ASGI],
|
||||
le protocole pour la passerelle asynchrone entre votre application et le serveur Web,
|
||||
* `+wsgi.py+` contient la définition de l’interface https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface[WSGI], qui
|
||||
* `wsgi.py` contient la définition de l’interface https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface[WSGI], qui
|
||||
permettra à votre serveur Web (Nginx, Apache, ...) de faire un pont vers votre projet.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
|
@ -97,24 +99,24 @@ Il est ainsi conseillé de créer un fichier `requirements.txt` qui pointera ver
|
|||
|
||||
Par soucis de propreté, placez ces différents fichiers dans un dossier `requirements`, que vous placerez à la racine de votre projet.
|
||||
|
||||
Au début de chaque fichier, il suffit d’ajouter la ligne `+-r base.txt+`, puis de lancer l’installation grâce à un
|
||||
`+pip install -r <nom du fichier>+`.
|
||||
Au début de chaque fichier, il suffit d’ajouter la ligne `-r base.txt`, puis de lancer l’installation grâce à un
|
||||
`pip install -r <nom du fichier>`.
|
||||
|
||||
A noter également qu'une bonne pratique consiste à placer un fichier `+requirements.txt+` à la racine du projet, et dans lequel nous
|
||||
retrouverons le contenu `+-r requirements/production.txt+`, notamment pour Heroku.
|
||||
A noter également qu'une bonne pratique consiste à placer un fichier `requirements.txt` à la racine du projet, et dans lequel nous
|
||||
retrouverons le contenu `-r requirements/production.txt`, notamment pour Heroku.
|
||||
====
|
||||
|
||||
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…) des dépendances à installer, pour peu que l’on reste sous un certain seuil.
|
||||
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...) des dépendances à installer, pour peu que l’on reste sous un certain seuil.
|
||||
|
||||
=== Django
|
||||
|
||||
Comme nous l’avons vu ci-dessus, `+django-admin+` permet de créer un nouveau projet.
|
||||
Comme nous l’avons vu ci-dessus, `django-admin` permet de créer un nouveau projet.
|
||||
Nous faisons ici une distinction entre un *projet* et une *application*:
|
||||
|
||||
* *Un projet* représente l’ensemble des applications, paramètres, middlewares, dépendances, …, qui font que votre code fait ce qu’il est sensé faire. Il s’agit grosso modo d’un câblage de tous les composants entre eux.
|
||||
* *Une application* est un contexte d’exécution (vues, comportements, pages HTML, …), idéalement autonome, d’une partie du projet. Une application est supposée avoir une portée de réutilisation, même s’il ne sera pas toujours possible de viser une généricité parfaite.
|
||||
* *Un projet* représente l’ensemble des applications, paramètres, middlewares, dépendances, ..., qui font que votre code fait ce qu’il est sensé faire. Il s’agit grosso modo d’un câblage de tous les composants entre eux.
|
||||
* *Une application* est un contexte d’exécution (vues, comportements, pages HTML, ...), idéalement autonome, d’une partie du projet. Une application est supposée avoir une portée de réutilisation, même s’il ne sera pas toujours possible de viser une généricité parfaite.
|
||||
|
||||
Pour `+gwift+`, nous aurons :
|
||||
Pour `gwift`, nous aurons :
|
||||
|
||||
.Projet Django vs Applications
|
||||
image::django/django-project-vs-apps-gwift.png[align="center"]
|
||||
|
@ -125,30 +127,19 @@ image::django/django-project-vs-apps-gwift.png[align="center"]
|
|||
. Voire une troisième application qui gérera les partages entre
|
||||
utilisateurs et listes.
|
||||
|
||||
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.
|
||||
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 `+khana+`, nous pourrions avoir quelque chose comme ceci:
|
||||
Pour `khana`, nous pourrions avoir quelque chose comme ceci :
|
||||
|
||||
.Django Project vs Applications
|
||||
image::django/django-project-vs-apps-khana.png[align="center"]
|
||||
|
||||
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 *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 https://www.djangopackages.com/[paquets Django] déjà
|
||||
disponibles: ce sont "_simplement_" de petites applications empaquetées
|
||||
et pouvant être réutilisées dans différents contextes (eg.
|
||||
https://github.com/tomchristie/django-rest-framework[Django-Rest-Framework],
|
||||
https://github.com/django-debug-toolbar/django-debug-toolbar[Django-Debug-Toolbar], ...)
|
||||
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 *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 https://www.djangopackages.com/[paquets Django] déjà disponibles: ce sont "_simplement_" de petites applications empaquetées et pouvant être réutilisées dans différents contextes (eg. https://github.com/tomchristie/django-rest-framework[Django-Rest-Framework], https://github.com/django-debug-toolbar/django-debug-toolbar[Django-Debug-Toolbar], ...).
|
||||
|
||||
Le projet s’occupe principalement d’appliquer une couche de glue entre
|
||||
différentes applications.
|
||||
Le projet s’occupe donc principalement d’appliquer une couche de glue entre différentes applications, là où l'application est sensée composer une entité autonome et réutilisable.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
|
@ -160,17 +151,17 @@ Une autre bonne pratique consiste à aussi *limiter à cinq* le nombre de modèl
|
|||
Tant que ce seuil ne sera pas atteint, ne vous occupez pas de ce principe.
|
||||
====
|
||||
|
||||
==== manage.py
|
||||
=== Commandes de gestion
|
||||
|
||||
Le fichier `+manage.py+` que vous trouvez à la racine de votre projet est un *wrapper* sur les commandes `+django-admin+`.
|
||||
Le fichier `manage.py` que vous trouvez à la racine de votre projet est un *wrapper* sur les commandes `django-admin`.
|
||||
A partir de maintenant, nous n’utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet :
|
||||
|
||||
* `+manage.py check+` pour vérifier (en surface…) que votre projet ne rencontre aucune erreur évidente
|
||||
* `+manage.py check --deploy+`, pour vérifier (en surface aussi) que l’application est prête pour un déploiement
|
||||
* `+manage.py runserver+` pour lancer un serveur de développement
|
||||
* `+manage.py test+` pour découvrir les tests unitaires disponibles et les lancer.
|
||||
* `manage.py check` pour vérifier (en surface...) que votre projet ne rencontre aucune erreur évidente
|
||||
* `manage.py check --deploy`, pour vérifier (en surface aussi) que l’application est prête pour un déploiement
|
||||
* `manage.py runserver` pour lancer un serveur de développement
|
||||
* `manage.py test` pour découvrir les tests unitaires disponibles et les lancer.
|
||||
|
||||
La liste complète peut être affichée avec `+manage.py help+`.
|
||||
La liste complète peut être affichée avec `manage.py help`.
|
||||
Vous remarquerez que ces commandes sont groupées selon différentes catégories:
|
||||
|
||||
* *auth*: création d’un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant,
|
||||
|
@ -180,11 +171,11 @@ Vous remarquerez que ces commandes sont groupées selon différentes catégories
|
|||
* *staticfiles*: gestion des fichiers statiques et lancement du serveur de développement.
|
||||
|
||||
Chaque section correspond à une application.
|
||||
En analysant le code, ces applications peuvent être trouvées sous `+django.contrib+`.
|
||||
En analysant le code, ces applications peuvent être trouvées sous `django.contrib`.
|
||||
|
||||
Nous verrons plus tard comment ajouter de nouvelles commandes.
|
||||
|
||||
Si nous démarrons la commande `+poetry run python manage.py runserver+`, nous verrons la sortie console suivante :
|
||||
Si nous démarrons la commande `poetry run python manage.py runserver`, nous verrons la sortie console suivante :
|
||||
|
||||
```bash
|
||||
$ poetry run python manage.py runserver
|
||||
|
@ -204,13 +195,13 @@ image::django/manage-runserver.png[align="center"]
|
|||
|
||||
[NOTE]
|
||||
====
|
||||
Si vous avez suivi les étapes jusqu’ici, vous avez également dû voir un message type `You have 18 unapplied migration(s).`
|
||||
Si vous avez suivi les étapes jusqu’ici, vous avez également dû voir un message type `You have 18 unapplied migration(s).`
|
||||
Cela concerne les migrations, et c’est un point que nous verrons un peu plus tard.
|
||||
====
|
||||
|
||||
=== Minimal Viable Application
|
||||
|
||||
Maintenant que nous avons a vu à quoi servait `+manage.py+`, nous pouvons créer notre nouvelle application grâce à la commande `poetry run python manage.py startapp <label>`.
|
||||
Maintenant que nous avons a vu à quoi servait `manage.py`, nous pouvons créer notre nouvelle application grâce à la commande `poetry run python manage.py startapp <label>`.
|
||||
|
||||
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.
|
||||
|
@ -224,147 +215,126 @@ $ poetry run python manage.py startapp wish gwift/wish <2>
|
|||
<2> Nous exécutons ensuite le _scaffolding_ de création de cette application, dans le répertoire pré-cité. Cette deuxième étape est également discutée en annexe, dans la section <<Différentes structures de projets>>.
|
||||
|
||||
|
||||
Django nous a créé un répertoire `wish`, dans lequel nous trouvons les fichiers et dossiers suivants :
|
||||
|
||||
* `wish/__init__.py` pour que notre répertoire `wish` soit converti en package Python,
|
||||
* `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.
|
||||
* `wish/apps.py` qui contient la configuration de l’application et qui permet notamment de fixer un nom ou un libellé https://docs.djangoproject.com/en/stable/ref/applications/
|
||||
* `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)
|
||||
* `wish/models.py` représentera et structurera nos données. Ce modèle est intimement lié aux migrations.
|
||||
* `wish/tests.py` pour les tests unitaires.
|
||||
|
||||
Résultat ? Django nous a créé un répertoire `+wish+`, dans lequel nous
|
||||
trouvons les fichiers et dossiers suivants:
|
||||
La structure de vos répertoires devient celle-ci :
|
||||
|
||||
* `+wish/__init__.py+` pour que notre répertoire `+wish+` soit converti
|
||||
en package Python.
|
||||
* `+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.
|
||||
* `+wish/apps.py+` qui contient la configuration de l’application et qui
|
||||
permet notamment de fixer un nom ou un libellé
|
||||
https://docs.djangoproject.com/en/stable/ref/applications/
|
||||
* `+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)
|
||||
* `+wish/models.py+` représentera et structurera nos données. Ce modèle
|
||||
est intimement lié aux migrations.
|
||||
* `+wish/tests.py+` pour les tests unitaires.
|
||||
|
||||
Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire
|
||||
`+wish+` dans votre répertoire `+gwift+` existant. C’est _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 `+gwift+` ! footnote:[Il manque quelques fichiers utiles, qui
|
||||
seront décrits par la suite, pour qu’une application soit réellement
|
||||
autonome: templates, `+urls.py+`, managers, services, …]
|
||||
```bash
|
||||
% tree .
|
||||
.
|
||||
├── db.sqlite3
|
||||
├── gwift
|
||||
│ ├── __init__.py
|
||||
│ ├── asgi.py
|
||||
│ ├── settings.py
|
||||
│ ├── urls.py
|
||||
│ ├── wish <1>
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── admin.py
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── migrations
|
||||
│ │ │ └── __init__.py
|
||||
│ │ ├── models.py
|
||||
│ │ ├── tests.py
|
||||
│ │ └── views.py
|
||||
│ └── wsgi.py
|
||||
├── manage.py
|
||||
├── poetry.lock
|
||||
└── pyproject.toml
|
||||
```
|
||||
<1> C'est dans ce répertoire que nous retrouvons les fichiers composant notre application.
|
||||
|
||||
=== 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, … 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 `+print()+` ou des `+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,
|
||||
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, ...
|
||||
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 `print()` ou des `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 `+Don’t 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).
|
||||
Django (et d’autres frameworks) résolvent ce problème en se basant ouvertement sur le principe de `Don’t repeat yourself` (((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.
|
||||
|
||||
*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 `+https://gwift/wishes/91827+`. Lorsque cette URL "arrive" dans
|
||||
notre application, son point d’entrée se trouvera au niveau des fichiers
|
||||
`+asgi.py+` ou `+wsgi.py+`. Nous verrons cette partie plus tard, et nous
|
||||
pouvons nous concentrer sur le chemin interne qu’elle va parcourir.
|
||||
Le chemin parcouru par une requête peut être repris schématiquement comme si *un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application*.
|
||||
Nous prendrons l’exemple de l’URL fictive `https://gwift/wishes/91827`.
|
||||
Lorsque cette URL "arrive" dans notre application, son point d’entrée se trouvera au niveau des fichiers `asgi.py` ou `wsgi.py`. Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu’elle va parcourir.
|
||||
|
||||
.How it works
|
||||
image::django/django-how-it-works.png[align="center"]
|
||||
|
||||
*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 `+gwift/urls.py+`.
|
||||
*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 `gwift/urls.py`.
|
||||
Ce fichier reprendra
|
||||
|
||||
[source,Python]
|
||||
----
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from gwift.views import wish_details
|
||||
```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),
|
||||
]
|
||||
----
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls), <1>
|
||||
path("wishes/<int:wish_id>", wish_details), <2>
|
||||
]
|
||||
```
|
||||
<1> Cette partie-ci est auto-générée, et sera vue dans le chapitre <<Administration>>
|
||||
<2> Nous insérons ici notre nouvelle URL (((URL)))
|
||||
|
||||
*Etape 1* - Si ce n’est pas le cas, l’application n’ira pas plus loin et
|
||||
retournera une erreur à l’utilisateur.
|
||||
*Etape 1* - Si ce n’est pas le cas, l’application n’ira pas plus loin et retournera une erreur à l’utilisateur.
|
||||
|
||||
*Etape 2* - Django va parcourir l’ensemble des _patterns_ présents dans
|
||||
le fichier `+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
|
||||
`+/wishes/91827+` a une correspondance au niveau de la ligne
|
||||
`+path("wishes/<int:wish_id>+` dans l’exemple ci-dessous. Django va
|
||||
alors appeler la fonction footnote:[Qui ne sera pas toujours une
|
||||
fonction. Django s’attend à trouver un _callable_, c’est-à-dire
|
||||
n’importe quel élément qu’il peut appeler comme une fonction.] associée
|
||||
à ce _pattern_, c’est-à-dire `+wish_details+` du module `+gwift.views+`.
|
||||
*Etape 2* - Django va parcourir l’ensemble des _patterns_ présents dans le fichier `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 `/wishes/91827` a une correspondance au niveau de la ligne `path("wishes/<int:wish_id>` dans l’exemple ci-dessous.
|
||||
Django va alors appeler la fonction correspondant au deuxième paramètre que nous trouvons dans l'appel à la fonction `path()`, c'est-à-dire ici la fonction `wish_details`, du module `gwift.views`.
|
||||
|
||||
* Nous importons la fonction `+wish_details+` du module `+gwift.views+`
|
||||
* Champomy et cotillons! Nous avons une correspondance avec
|
||||
`+wishes/details/91827+`
|
||||
[NOTE]
|
||||
====
|
||||
Ce deuxième paramètre ne sera pas toujours une fonction.
|
||||
Django s’attend à trouver un _callable_, c’est-à-dire n’importe quel élément qu’il peut appeler comme une fonction, associée à ce _pattern_.
|
||||
====
|
||||
|
||||
Le module `+gwift.views+` qui se trouve dans le fichier
|
||||
`+gwift/views.py+` peut ressembler à ceci:
|
||||
* Nous importons la fonction `wish_details` du module `gwift.views`,
|
||||
* Champomy et cotillons ! Nous avons une correspondance avec `wishes/details/91827`.
|
||||
|
||||
[source,Python]
|
||||
----
|
||||
# gwift/views.py
|
||||
Le module `gwift.views` qui se trouve dans le fichier `gwift/views.py` peut ressembler à ceci:
|
||||
|
||||
[...]
|
||||
```python
|
||||
# gwift/views.py
|
||||
|
||||
from datetime import datetime
|
||||
[...]
|
||||
|
||||
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
|
||||
)
|
||||
----
|
||||
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
|
||||
)
|
||||
```
|
||||
|
||||
Pour résumer, cette fonction permet:
|
||||
Pour résumer, cette fonction permet :
|
||||
|
||||
. De construire un _contexte_, qui est représenté sous la forme d’un
|
||||
dictionnaire associant des clés à des valeurs. Les clés sont
|
||||
respectivement `+user_name+`, `+user_first_name+` et `+now+`, tandis que
|
||||
leurs valeurs respectives sont `+Bond+`, `+James+` et le
|
||||
`+moment présent+` footnote:[Non, pas celui d’Eckhart Tolle].
|
||||
. Nous passons ensuite ce dictionnaire à un canevas,
|
||||
`+wish_details.html+`, que l’on trouve normalement dans le répertoire
|
||||
`+templates+` de notre projet, ou dans le répertoire `+templates+`
|
||||
propre à notre application.
|
||||
. L’application du contexte sur le canevas au travers de la fonction
|
||||
`+render+` nous donne un résultat formaté.
|
||||
. De construire un _contexte_, qui est représenté sous la forme d’un dictionnaire associant des clés à des valeurs. Les clés sont
|
||||
respectivement `user_name`, `user_first_name` et `now`, tandis que leurs valeurs respectives sont `Bond`, `James` et le
|
||||
`moment présent` footnote:[Non, pas celui d’Eckhart Tolle].
|
||||
. Nous passons ensuite ce dictionnaire à un canevas, `wish_details.html`, que l’on trouve normalement dans le répertoire
|
||||
`templates` de notre projet, ou dans le répertoire `templates` propre à notre application.
|
||||
. L’application du contexte sur le canevas au travers de la fonction `render` nous donne un résultat formaté.
|
||||
|
||||
[source,html]
|
||||
----
|
||||
<!-- fichier wish_details.html -->
|
||||
<!-- templates/wish_details.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
@ -378,9 +348,7 @@ propre à notre application.
|
|||
</html>
|
||||
----
|
||||
|
||||
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:
|
||||
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 :
|
||||
|
||||
[source,html]
|
||||
----
|
||||
|
@ -398,365 +366,7 @@ la requête initiale:
|
|||
----
|
||||
|
||||
.Résultat
|
||||
image::images/django/django-first-template.png[images/django/django-first-template]
|
||||
|
||||
=== Configuration globale
|
||||
|
||||
==== Structure finale
|
||||
|
||||
En repartant de la structure initiale décrite au chapitre précédent,
|
||||
nous arrivons à ceci.
|
||||
|
||||
TODO : passer à poetry
|
||||
|
||||
==== 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, …
|
||||
|
||||
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
|
||||
https://cookiecutter.readthedocs.io/[Cookie-Cutter], qui se base sur des
|
||||
canevas _type 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 https://cookiecutter-django.readthedocs.io[ceux qui
|
||||
existent déjà].
|
||||
|
||||
Pour démarrer, créez un environnement virtuel (comme d’habitude):
|
||||
|
||||
....
|
||||
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!
|
||||
....
|
||||
|
||||
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 `+--template+`, suivie d’un
|
||||
argument reprenant le nom de votre projet (`+<my_project>+`), lors de
|
||||
l’initialisation d’un projet avec la commande `+startproject+` de
|
||||
`+django-admin+`, afin de calquer votre arborescence sur un projet
|
||||
existant. La
|
||||
https://docs.djangoproject.com/en/stable/ref/django-admin/#startproject[documentation]
|
||||
à ce sujet est assez complète.
|
||||
|
||||
....
|
||||
django-admin.py startproject --template=https://[...].zip <my_project>
|
||||
....
|
||||
|
||||
=== 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 *tests.py*, qui
|
||||
inclut la classe `+TestCase+` depuis le package `+django.test+`:
|
||||
|
||||
On a deux choix ici:
|
||||
|
||||
. Utiliser les librairies de test de Django
|
||||
. Utiliser Pytest
|
||||
|
||||
==== django.test
|
||||
|
||||
[source,Python]
|
||||
----
|
||||
from django.test import TestCase
|
||||
class TestModel(TestCase):
|
||||
def test_str(self):
|
||||
raise NotImplementedError('Not implemented yet')
|
||||
----
|
||||
|
||||
==== Pytest
|
||||
|
||||
==== Couverture de code
|
||||
|
||||
|
||||
....
|
||||
# requirements/base.text
|
||||
[...]
|
||||
django_coverage_plugin
|
||||
....
|
||||
|
||||
....
|
||||
# .coveragerc to control coverage.py
|
||||
[run]
|
||||
branch = True
|
||||
omit = ../*migrations*
|
||||
plugins =
|
||||
django_coverage_plugin
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
directory = coverage_html_report
|
||||
....
|
||||
|
||||
|
||||
|
||||
==== Recommandations sur les tests
|
||||
|
||||
En résumé, il est recommandé de:
|
||||
|
||||
. Tester que le nommage d’une URL (son attribut `+name+` dans les
|
||||
fichiers `+urls.py+`) corresponde à la fonction que l’on y a définie
|
||||
. Tester que l’URL envoie bien vers l’exécution d’une fonction (et que
|
||||
cette fonction est celle que l’on attend)
|
||||
|
||||
===== Tests de nommage
|
||||
|
||||
[source,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)
|
||||
----
|
||||
|
||||
===== Tests d’URLs
|
||||
|
||||
[source,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)
|
||||
----
|
||||
|
||||
==== Couverture de code
|
||||
|
||||
Pour l’exemple, nous allons écrire la fonction
|
||||
`+percentage_of_completion+` sur la classe `+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:
|
||||
|
||||
[source,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
|
||||
----
|
||||
|
||||
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
|
||||
|
||||
....
|
||||
$ 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%
|
||||
....
|
||||
|
||||
Si vous générez le rapport HTML avec la commande `+coverage html+` et
|
||||
que vous ouvrez le fichier
|
||||
`+coverage_html_report/src_wish_models_py.html+`, vous verrez que les
|
||||
méthodes en rouge ne sont pas testées. *A contrario*, la couverture de
|
||||
code atteignait *98%* avant l’ajout de cette nouvelle méthode.
|
||||
|
||||
Pour cela, on va utiliser un fichier `+tests.py+` dans notre application
|
||||
`+wish+`. *A priori*, ce fichier est créé automatiquement lorsque vous
|
||||
initialisez une nouvelle application.
|
||||
|
||||
[source,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)
|
||||
----
|
||||
|
||||
L’attribut `+@property+` sur la méthode `+percentage_of_completion()+`
|
||||
va nous permettre d’appeler directement la méthode
|
||||
`+percentage_of_completion()+` comme s’il s’agissait d’une propriété de
|
||||
la classe, au même titre que les champs `+number_of_parts+` ou
|
||||
`+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
|
||||
*cache*, que nous aborderons plus loin.
|
||||
|
||||
En relançant la couverture de code, on voit à présent que nous arrivons
|
||||
à 99%:
|
||||
|
||||
....
|
||||
$ 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%
|
||||
....
|
||||
|
||||
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
|
||||
link:#SRP[[SRP]], c’est le framework lui-même qui doit en assurer la
|
||||
maintenance et le bon fonctionnement.
|
||||
|
||||
=== 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.
|
||||
|
||||
_React, for example, has an additional clause that could potentially
|
||||
cause patent claim conflicts with React users_ . Cette issue a été
|
||||
adressée en 2017
|
||||
footnote:[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:
|
||||
|
||||
. *MIT*
|
||||
. *GPLv3*
|
||||
. *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:[_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 -
|
||||
https://fair.io_]
|
||||
. *WTFPL*
|
||||
|
||||
Mike Perham, qui maintient Sidekiq, a ainsi proposé une forme de dualité
|
||||
entre la mise à disposition du code source et son utilisation :
|
||||
|
||||
____
|
||||
_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._
|
||||
|
||||
_...[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._
|
||||
____
|
||||
image::django/django-first-template.png[images/django/django-first-template]
|
||||
|
||||
=== Conclusions
|
||||
|
||||
|
@ -764,4 +374,4 @@ 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, …
|
||||
d’ajouter un mécanisme de mise en cache, des logiciels de suivi, ...
|
||||
|
|
|
@ -217,7 +217,7 @@ sortira très bien et sera beaucoup plus efficace que n’importe quelle
|
|||
chaîne de caractères que vous pourriez indiquer et qui sera fausse dans
|
||||
six mois,
|
||||
* **Inutile de décrire quelque chose qui est évident** : documenter la
|
||||
méthode `+get_age()+` d’une personne n’aura pas beaucoup d’intérêt,
|
||||
méthode `get_age()` d’une personne n’aura pas beaucoup d’intérêt,
|
||||
* S’il est nécessaire de décrire un comportement au sein-même d’une
|
||||
fonction, avec un commentaire _inline_, c’est que ce comportement
|
||||
pourrait être extrait dans une nouvelle fonction (qui, elle, pourra être
|
||||
|
@ -230,7 +230,7 @@ Il existe plusieurs types de balisages reconnus :
|
|||
|
||||
. RestructuredText
|
||||
. Numpy
|
||||
. Google Style (parfois connue sous l’intitulé `+Napoleon+`)
|
||||
. Google Style (parfois connue sous l’intitulé `Napoleon`)
|
||||
|
||||
... mais tout système de balisage peut être reconnu, sous réseve de
|
||||
respecter la structure de la https://peps.python.org/pep-0257/[PEP257], qui donne des recommandations haut-niveau concernant la structure des docstrings : ce qu’elles doivent contenir et comment l’expliciter, sans imposer quelle que mise en forme de contenu que ce soit.
|
||||
|
@ -261,13 +261,13 @@ la manière dont l’implémentation est réalisée). (_Extended summary_)
|
|||
* La ou les valeurs de retour, accompagnées de leur type et d’une
|
||||
description (_Returns_)
|
||||
* Les valeurs générées (_yields_) dans le cas de générateurs (_Yields_)
|
||||
* Un objet attendu par la méthode `+send()+` (toujours dans le cas de
|
||||
* Un objet attendu par la méthode `send()` (toujours dans le cas de
|
||||
générateurs) (_Receives_)
|
||||
* D’autres paramètres, notamment dans le cas des paramètres `+*args+` et
|
||||
`+**kwargs+`. (_Other parameters_)
|
||||
* D’autres paramètres, notamment dans le cas des paramètres `*args` et
|
||||
`**kwargs`. (_Other parameters_)
|
||||
* Les exceptions levées (_Raises_)
|
||||
* Les avertissements destinés à l’utilisateur (_Warnings_)
|
||||
* Une section "Pour continuer…" (_See also_)
|
||||
* Une section "Pour continuer..." (_See also_)
|
||||
* Des notes complémentaires (_Notes_)
|
||||
* Des références (_References_)
|
||||
* Des exemples (_Examples_)
|
||||
|
@ -497,7 +497,7 @@ test.py:18:1: W391 blank line at end of file
|
|||
|
||||
Nous trouvons des erreurs:
|
||||
|
||||
* de *conventions*: le nombre de lignes qui séparent deux fonctions, le nombre d’espace après un opérateur, une ligne vide à la fin du fichier, … Ces _erreurs_ n’en sont pas vraiment, elles indiquent juste de
|
||||
* de *conventions*: le nombre de lignes qui séparent deux fonctions, le nombre d’espace après un opérateur, une ligne vide à la fin du fichier, ... Ces _erreurs_ n’en sont pas vraiment, elles indiquent juste de
|
||||
potentiels problèmes de communication si le code devait être lu ou compris par une autre personne.
|
||||
* de *définition*: une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug
|
||||
potentiel. Ne pas en tenir compte nuira sans doute à la santé de votre code (et risque de vous réveiller à cinq heures du mat’, quand votre application se prendra méchamment les pieds dans le tapis).
|
||||
|
@ -552,7 +552,7 @@ mises en forme, que l’on a vues ci-dessus)
|
|||
que le traitement n’a pas pu aboutir.
|
||||
|
||||
Pylint propose également une option particulièrement efficace, qui prend
|
||||
le paramètre `+–errors-only+`, et qui n’affiche que les occurrences
|
||||
le paramètre `–errors-only`, et qui n’affiche que les occurrences
|
||||
appartenant à la catégorie *E*.
|
||||
Connaissant ceci, il est extrêmement pratique d’intégrer cette option au niveau des processus d’intégration continue, puisque la présence d’une erreur détectée par Pylint implique généralement un correctif à appliquer.
|
||||
|
||||
|
@ -563,7 +563,7 @@ peut être:
|
|||
|
||||
. *Une ligne de code*
|
||||
. *Un bloc de code* : une fonction, une méthode, une classe, un module,
|
||||
…
|
||||
...
|
||||
. *Un projet entier*
|
||||
|
||||
Lui, c’est le petit chouchou à la mode.
|
||||
|
@ -612,9 +612,9 @@ content instead.
|
|||
____
|
||||
|
||||
Traduit rapidement à partir de la langue de Batman :
|
||||
"`+En utilisant Black, vous cédez le contrôle sur le formatage de votre code. En retour, Black vous fera gagner un max de temps, diminuera votre charge mentale et fera revenir l’être aimé+`".
|
||||
"`En utilisant Black, vous cédez le contrôle sur le formatage de votre code. En retour, Black vous fera gagner un max de temps, diminuera votre charge mentale et fera revenir l’être aimé`".
|
||||
Mais la partie réellement intéressante concerne le fait que
|
||||
"`+Tout code qui sera passé par Black aura la même forme, indépendamment du project sur lequel vous serez en train de travailler. L’étape de formatage deviendra transparente, et vous pourrez vous concentrer sur le contenu+`".
|
||||
"`Tout code qui sera passé par Black aura la même forme, indépendamment du project sur lequel vous serez en train de travailler. L’étape de formatage deviendra transparente, et vous pourrez vous concentrer sur le contenu`".
|
||||
|
||||
=== Typage statique
|
||||
|
||||
|
@ -651,7 +651,7 @@ a
|
|||
|
||||
Malgré que nos annotations déclarent une liste d’entiers, rien ne nous empêche de lui envoyer une liste de caractères, sans que cela ne lui pose de problèmes. La signature de notre fonction n’est donc pas cohérente avec son comportement.
|
||||
|
||||
Est-ce que Mypy va râler ? *Oui, aussi*. Non seulement nous retournons la valeur `+None+` si la liste est vide alors que nous lui annoncions un entier en sortie, mais en plus, nous l’appelons avec une liste de caractères, alors que nous nous attendons à une liste d’entiers :
|
||||
Est-ce que Mypy va râler ? *Oui, aussi*. Non seulement nous retournons la valeur `None` si la liste est vide alors que nous lui annoncions un entier en sortie, mais en plus, nous l’appelons avec une liste de caractères, alors que nous nous attendons à une liste d’entiers :
|
||||
|
||||
....
|
||||
>>> mypy mypy-test.py
|
||||
|
@ -664,8 +664,8 @@ Found 4 errors in 1 file (checked 1 source file)
|
|||
|
||||
Pour corriger ceci, nous devons :
|
||||
|
||||
. Importer le type `+Optional+` et l’utiliser en sortie de notre
|
||||
fonction `+first_int_elem+`
|
||||
. Importer le type `Optional` et l’utiliser en sortie de notre
|
||||
fonction `first_int_elem`
|
||||
. Eviter de lui donner de mauvais paramètres ;-)²
|
||||
|
||||
Mypy demande une rigueur supplémentaire, qui peut s'avérer extrêmement utile sur des grosses bases de code ou avec de larges équipes, mais qui nécessite également un gros travail d'intégration, qui pourrait potentiellement ralentir le travail.
|
||||
|
@ -848,4 +848,4 @@ facteurs → Construction du fichier setup.cfg
|
|||
django_coverage_plugin
|
||||
....
|
||||
|
||||
Mypy + black + pylint + flake8 + pyflakes + …
|
||||
Mypy + black + pylint + flake8 + pyflakes + ...
|
||||
|
|
|
@ -51,7 +51,7 @@ fonction, et ne doit pas obligatoirement faire partie d’une classe héritant d
|
|||
* Des _fixtures_ faciles à réutiliser entre vos différents composants,
|
||||
* Une compatibilité avec le reste de l’écosystème, dont la couverture de code présentée ci-dessous.
|
||||
|
||||
Ainsi, après installation, il nous suffit de créer notre module `+test_models.py+`, dans lequel nous allons simplement tester l’addition d’un nombre et d’une chaîne de caractères (oui, c’est complètement
|
||||
Ainsi, après installation, il nous suffit de créer notre module `test_models.py`, dans lequel nous allons simplement tester l’addition d’un nombre et d’une chaîne de caractères (oui, c’est complètement
|
||||
biesse; on est sur la partie théorique ici):
|
||||
|
||||
[source,Python]
|
||||
|
@ -108,7 +108,7 @@ Il ne s’agit pas de vérifier que le code est bien testé, mais de vérifier q
|
|||
|
||||
Le paquet `coverage` se charge d’évaluer le pourcentage de code couvert par les tests.
|
||||
Avec pytest, il convient d’utiliser le paquet pytest-cov, suivi de la commande pytest
|
||||
`+–cov=gwift tests/+`.
|
||||
`–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
|
||||
|
@ -144,11 +144,11 @@ $ coverage report
|
|||
$ coverage html
|
||||
....
|
||||
|
||||
Avec `+pytest+`, il convient d’utiliser le paquet https://pypi.org/project/pytest-cov/[`+pytest-cov+`], suivi de la commande `+pytest --cov=gwift tests/+`.
|
||||
Avec `pytest`, il convient d’utiliser le paquet https://pypi.org/project/pytest-cov/[`pytest-cov`], suivi de la commande `pytest --cov=gwift tests/`.
|
||||
|
||||
Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer par le paquethttps://pypi.org/project/django-coverage-plugin/[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 l’exécution.
|
||||
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 l’exécution.
|
||||
|
||||
=== Matrice de compatibilité
|
||||
|
||||
|
@ -173,7 +173,7 @@ La documentation de Poetry spécifie https://python-poetry.org/docs/faq/#is-tox-
|
|||
|
||||
A noter que pour que les commandes ci-dessus fonctionnent correctement, il sera nécessaire que vous ayez les différentes versions d’interpréteurs installées ou accessibles.
|
||||
|
||||
Une solution consiste à utiliser `pyenv`, qui s'occupera d'installer les versions devant l'être, afin d'éviter les erreurs de type `ERROR: pyXX: InterpreterNotFound: pythonX.X` :
|
||||
Une solution consiste à utiliser `pyenv`, qui s'occupera d'installer les versions devant l'être, afin d'éviter les erreurs de type `ERROR: pyXX: InterpreterNotFound: pythonX.X` :
|
||||
|
||||
....
|
||||
pyenv install 3.10
|
||||
|
@ -196,3 +196,244 @@ Pour atteindre cet objectif, il est nécessaire :
|
|||
. De disposer de tests automatisés,
|
||||
. De scénarii de déploiement,
|
||||
. D'une télémétrie omniprésente.
|
||||
|
||||
=== Tests
|
||||
|
||||
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 *tests.py*, qui inclut la classe `TestCase` depuis le package `django.test` :
|
||||
|
||||
On a deux choix ici :
|
||||
|
||||
. Utiliser les librairies de test de Django
|
||||
. Utiliser Pytest
|
||||
|
||||
=== django.test
|
||||
|
||||
[source,Python]
|
||||
----
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestModel(TestCase):
|
||||
def test_str(self):
|
||||
raise NotImplementedError('Not implemented yet')
|
||||
----
|
||||
|
||||
=== Pytest
|
||||
|
||||
|
||||
|
||||
=== Couverture de code
|
||||
|
||||
Pour valider la couverture de code, nous aurons besoin de `coverage` et de `django_coverage_plugin`.
|
||||
Ajoutez les au groupe de dépendances en test uniquement, puis ajoutez un fichier de configuration, qui sera valable quel que soit l'environnement de développement qui sera concerné :
|
||||
|
||||
....
|
||||
# .coveragerc
|
||||
|
||||
[run]
|
||||
branch = True
|
||||
omit = ../*migrations*
|
||||
plugins =
|
||||
django_coverage_plugin
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
[html]
|
||||
directory = coverage_html_report
|
||||
....
|
||||
|
||||
|
||||
|
||||
==== Recommandations sur les tests
|
||||
|
||||
En résumé, il est recommandé de:
|
||||
|
||||
. Tester que le nommage d’une URL (son attribut `name` dans les
|
||||
fichiers `urls.py`) corresponde à la fonction que l’on y a définie
|
||||
. Tester que l’URL envoie bien vers l’exécution d’une fonction (et que
|
||||
cette fonction est celle que l’on attend)
|
||||
|
||||
===== Tests de nommage
|
||||
|
||||
[source,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)
|
||||
----
|
||||
|
||||
===== Tests d’URLs
|
||||
|
||||
[source,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)
|
||||
----
|
||||
|
||||
==== Couverture de code
|
||||
|
||||
Pour l’exemple, nous allons écrire la fonction
|
||||
`percentage_of_completion` sur la classe `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:
|
||||
|
||||
[source,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
|
||||
----
|
||||
|
||||
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
|
||||
|
||||
....
|
||||
$ 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%
|
||||
....
|
||||
|
||||
Si vous générez le rapport HTML avec la commande `coverage html` et
|
||||
que vous ouvrez le fichier
|
||||
`coverage_html_report/src_wish_models_py.html`, vous verrez que les
|
||||
méthodes en rouge ne sont pas testées. *A contrario*, la couverture de
|
||||
code atteignait *98%* avant l’ajout de cette nouvelle méthode.
|
||||
|
||||
Pour cela, on va utiliser un fichier `tests.py` dans notre application
|
||||
`wish`. *A priori*, ce fichier est créé automatiquement lorsque vous
|
||||
initialisez une nouvelle application.
|
||||
|
||||
[source,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)
|
||||
----
|
||||
|
||||
L’attribut `@property` sur la méthode `percentage_of_completion()`
|
||||
va nous permettre d’appeler directement la méthode
|
||||
`percentage_of_completion()` comme s’il s’agissait d’une propriété de
|
||||
la classe, au même titre que les champs `number_of_parts` ou
|
||||
`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
|
||||
*cache*, que nous aborderons plus loin.
|
||||
|
||||
En relançant la couverture de code, on voit à présent que nous arrivons
|
||||
à 99%:
|
||||
|
||||
....
|
||||
$ 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%
|
||||
....
|
||||
|
||||
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
|
||||
link:#SRP[[SRP]], c’est le framework lui-même qui doit en assurer la
|
||||
maintenance et le bon fonctionnement.
|
||||
|
|
|
@ -28,9 +28,9 @@ Because value is created only when our services are running into production, we
|
|||
— DevOps Handbook Introduction
|
||||
____
|
||||
|
||||
Le serveur que Django met à notre disposition _via_ la commande `+runserver+` est extrêmement pratique, mais il est uniquement prévu pour la phase développement. En production :
|
||||
Le serveur que Django met à notre disposition _via_ la commande `runserver` est extrêmement pratique, mais il est uniquement prévu pour la phase développement. En production :
|
||||
|
||||
* Il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, … De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l’on connait ses limites et que l’on se limite à un utilisateur à la fois.
|
||||
* Il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ... De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l’on connait ses limites et que l’on se limite à un utilisateur à la fois.
|
||||
* Il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanés. En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu’un autre processus a déjà pris la main pour écrire ses données. En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou
|
||||
ou simplement parce que le serveur répondra trop lentement.
|
||||
|
||||
|
@ -79,7 +79,7 @@ Vous noterez que l’ensemble des composants utilisés sont _Open source_ :
|
|||
____
|
||||
When I built my first company starting in 1999, it cost $2.5 million in
|
||||
infrastructure just to get started and another $2.5 million in team
|
||||
costs to code, launch, manage, market and sell our software […].
|
||||
costs to code, launch, manage, market and sell our software [...].
|
||||
|
||||
The first major change in our industry was imperceptible to us as an
|
||||
industry. It was driven by the introduction of open-source software,
|
||||
|
|
|
@ -37,3 +37,5 @@ include::book/django-principles/migrations.adoc[]
|
|||
include::book/django-principles/querysets+managers.adoc[]
|
||||
|
||||
include::book/django-principles/forms.adoc[]
|
||||
|
||||
include::book/django-principles/admin.adoc[]
|
||||
|
|
|
@ -55,8 +55,8 @@ include::book/environment/development-tools.adoc[]
|
|||
|
||||
include::book/environment/python.adoc[]
|
||||
|
||||
include::book/environment/tests.adoc[]
|
||||
|
||||
include::book/environment/gdpr.adoc[]
|
||||
|
||||
include::book/environment/new-project.adoc[]
|
||||
|
||||
include::book/environment/tests.adoc[]
|
||||
|
|
Loading…
Reference in New Issue