Include new-project and python
This commit is contained in:
parent
3e1da2d799
commit
e583606146
|
@ -313,6 +313,33 @@ Il est (relativement) léger, et permet en plus de générer le code associé à
|
|||
|
||||
image::environment/insomnia.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:
|
||||
|
||||
....
|
||||
# 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
|
||||
|
||||
.PHONY: help coverage
|
||||
|
||||
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."
|
||||
....
|
||||
|
||||
Pour la petite histoire, `+make+` peu sembler un peu désuet, mais reste
|
||||
extrêmement efficace.
|
||||
|
||||
=== Conclusions
|
||||
|
||||
Pour résumer :
|
||||
|
|
|
@ -0,0 +1,873 @@
|
|||
== Démarrer un nouveau projet
|
||||
|
||||
Django fonctionne sur un
|
||||
https://docs.djangoproject.com/en/dev/internals/release-process/[roulement
|
||||
de trois versions mineures pour une version majeure], clôturé par une
|
||||
version LTS (_Long Term Support_).
|
||||
|
||||
=== Gestion des dépendances
|
||||
|
||||
Comme nous en avons déjà discuté, Poetry est la solution que nous avons
|
||||
choisie pour la gestion de nos dépendances. Pour installer une nouvelle
|
||||
librairie, vous pouvez simplement passer par la commande
|
||||
`+pip install <my_awesome_library>+`. Dans le cas de Django, et après
|
||||
avoir activé l’environnement, nous pouvons à présent y installer Django.
|
||||
Comme expliqué ci-dessus, la librairie restera indépendante du reste du
|
||||
système, et ne polluera aucun autre projet. nous exécuterons donc la
|
||||
commande suivante:
|
||||
|
||||
....
|
||||
$ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift-
|
||||
env/Scrips/activate.bat pour Windows.
|
||||
$ pip install django
|
||||
Collecting django
|
||||
Downloading Django-3.1.4
|
||||
100% |################################|
|
||||
Installing collected packages: django
|
||||
Successfully installed django-3.1.4
|
||||
....
|
||||
|
||||
Ici, la commande `+pip install django+` récupère la *dernière version
|
||||
connue disponible dans les dépôts https://pypi.org/* (sauf si vous en
|
||||
avez définis d’autres. Mais c’est hors sujet). Nous en avons déjà
|
||||
discuté : il est important de bien spécifier la version que vous
|
||||
souhaitez utiliser, sans quoi vous risquez de rencontrer des effets de
|
||||
bord.
|
||||
|
||||
L’installation de Django a ajouté un nouvel exécutable:
|
||||
`+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+`.
|
||||
|
||||
Pour démarrer notre projet, nous lançons
|
||||
`+django-admin startproject gwift+`:
|
||||
|
||||
....
|
||||
$ django-admin startproject gwift
|
||||
....
|
||||
|
||||
Cette action a pour effet de créer un nouveau dossier `+gwift+`, dans
|
||||
lequel nous trouvons la structure suivante :
|
||||
|
||||
....
|
||||
$ tree gwift
|
||||
gwift
|
||||
-- gwift
|
||||
----- asgi.py
|
||||
----- __init__.py
|
||||
----- settings.py
|
||||
----- urls.py
|
||||
----- wsgi.py
|
||||
-- manage.py
|
||||
....
|
||||
|
||||
C’est dans ce répertoire que vont vivre tous les fichiers liés au
|
||||
projet. Le but est de faire en sorte que toutes les opérations
|
||||
(maintenance, déploiement, écriture, tests, …) puissent se faire à
|
||||
partir d’un seul point d’entrée.
|
||||
|
||||
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
|
||||
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],
|
||||
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
|
||||
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.
|
||||
|
||||
Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel
|
||||
nous stockerons les dépendances et un fichier README:
|
||||
|
||||
TODO
|
||||
|
||||
Comme nous venons d’ajouter une dépendance à notre projet, profitons-en
|
||||
pour créer un fichier reprenant tous les dépendances de notre projet.
|
||||
Celles-ci sont normalement placées dans un fichier `+requirements.txt+`.
|
||||
Dans un premier temps, ce fichier peut être placé directement à la
|
||||
racine du projet, mais on préférera rapidement le déplacer dans un
|
||||
sous-répertoire spécifique (`+requirements+`), afin de grouper les
|
||||
dépendances en fonction de leur environnement de destination:
|
||||
|
||||
* `+base.txt+`
|
||||
* `+dev.txt+`
|
||||
* `+production.txt+`
|
||||
|
||||
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>+`. De cette manière, il est tout à
|
||||
fait acceptable de n’installer `+flake8+` et `+django-debug-toolbar+`
|
||||
qu’en développement par exemple. Dans l’immédiat, nous allons ajouter
|
||||
`+django+` dans une version égale à la version 3.2 dans le fichier
|
||||
`+requirements/base.txt+`.
|
||||
|
||||
....
|
||||
$ echo 'django==3.2' > requirements/base.txt
|
||||
$ echo '-r base.txt' > requirements/prod.txt
|
||||
$ echo '-r base.txt' > requirements/dev.txt
|
||||
....
|
||||
|
||||
Une bonne pratique consiste à également placer un fichier
|
||||
`+requirements.txt+` à la racine du projet, et dans lequel nous
|
||||
retrouverons le contenu `+-r requirements/production.txt+` (notamment
|
||||
pour Heroku).
|
||||
|
||||
Prenez directement l’habitude de spécifier la version ou les versions
|
||||
compatibles: les librairies que vous utilisez comme dépendances
|
||||
évoluent, de la même manière que vos projets. Pour être sûr et certain
|
||||
le code que vous avez écrit continue à fonctionner, spécifiez la version
|
||||
de chaque librairie de dépendances. Entre deux versions d’une même
|
||||
librairie, des fonctions sont cassées, certaines signatures sont
|
||||
modifiées, des comportements sont altérés, etc. Il suffit de
|
||||
parcourirles pages de _Changements incompatibles avec les anciennes
|
||||
versions dans Django_
|
||||
https://docs.djangoproject.com/fr/3.1/releases/3.0/[(par exemple ici
|
||||
pour le passage de la 3.0 à la 3.1)] pour réaliser que certaines
|
||||
opérations ne sont pas anodines, et que sans filet de sécurité, c’est le
|
||||
mur assuré. Avec les mécanismes d’intégration continue et de tests
|
||||
unitaires, nous verrons plus loin comment se prémunir d’un changement
|
||||
inattendu.
|
||||
|
||||
image:images/django-support-lts.png[image]
|
||||
|
||||
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.
|
||||
|
||||
Dans les étapes ci-dessous, nous épinglerons une version LTS afin de
|
||||
nous assurer une certaine sérénité d’esprit (= dont nous ne occuperons
|
||||
pas pendant les 3 prochaines années).
|
||||
|
||||
=== Django
|
||||
|
||||
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.
|
||||
|
||||
Pour `+gwift+`, nous aurons :
|
||||
|
||||
.Projet Django vs Applications
|
||||
image::images/django/django-project-vs-apps-gwift.png[images/django/django-project-vs-apps-gwift]
|
||||
|
||||
. Une première application pour la gestion des listes de souhaits et des
|
||||
éléments,
|
||||
. Une deuxième application pour la gestion des utilisateurs,
|
||||
. 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.
|
||||
|
||||
Pour `+khana+`, nous pourrions avoir quelque chose comme ceci:
|
||||
|
||||
.Django Project vs Applications
|
||||
image::images/django/django-project-vs-apps-khana.png[images/django/django-project-vs-apps-khana]
|
||||
|
||||
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. Découper proprement un projet en plusieurs
|
||||
applications totalement autonomes est illusoire. Une bonne pratique
|
||||
consiste à rester pragmatique et à partir avec *une seule* application,
|
||||
et la découper lorsque vous jugerez qu’elle grossit trop ou trop
|
||||
rapidement : découper trop rapidement et sans raison valable une
|
||||
application en plein de petits fichiers va gâcher énormément de temps de
|
||||
développement, sans apporter de réels bénéfices. D’autre part, une
|
||||
(autre) bonne pratique consiste à aussi *limiter à cinq* le nombre de
|
||||
modèles différents dans chaque application. Tant que ce seuil ne sera
|
||||
pas atteint, laissez ce principe de côté.
|
||||
|
||||
==== manage.py
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
* *django*: vérifier la *conformité* du projet, lancer un *shell*,
|
||||
*dumper* les données de la base, effectuer une migration du schéma, … Ce
|
||||
sont des commandes d’administration générale.
|
||||
* *sessions*: suppressions des sessions en cours
|
||||
* *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+`.
|
||||
|
||||
Nous verrons plus tard comment ajouter de nouvelles commandes.
|
||||
|
||||
Si nous démarrons la commande `+python manage.py runserver+`, nous
|
||||
verrons la sortie console suivante:
|
||||
|
||||
....
|
||||
$ python manage.py runserver
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
System check identified no issues (0 silenced).
|
||||
[...]
|
||||
December 15, 2020 - 20:45:07
|
||||
Django version 3.1.4, using settings 'gwift.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
Quit the server with CTRL-BREAK.
|
||||
....
|
||||
|
||||
Si nous nous rendons sur la page http://127.0.0.1:8000 (ou
|
||||
http://localhost:8000) comme le propose si gentiment notre (nouveau)
|
||||
meilleur ami, nous verrons ceci:
|
||||
|
||||
.python manage.py runserver (Non, ce n’est pas Challenger)
|
||||
image::images/django/manage-runserver.png[images/django/manage-runserver]
|
||||
|
||||
Nous avons mis un morceau de la sortie console entre crochet `+[+``+…+`
|
||||
ci-dessus, car elle concerne les migrations. Si vous avez suivi les
|
||||
étapes jusqu’ici, vous avez également dû voir un message type
|
||||
`+You have 18 unapplied migration(s). +``+[+``+… Run python manage.py migrate to apply them.+`
|
||||
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
|
||||
`+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. Pour nous, ce sera donc `+wish+`.
|
||||
|
||||
....
|
||||
$ python manage.py startapp wish
|
||||
....
|
||||
|
||||
Résultat? 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.
|
||||
|
||||
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, …]
|
||||
|
||||
=== 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,
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
.How it works
|
||||
image::images/diagrams/django-how-it-works.png[images/diagrams/django-how-it-works]
|
||||
|
||||
*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+`.
|
||||
|
||||
[source,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),
|
||||
]
|
||||
----
|
||||
|
||||
*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+`.
|
||||
|
||||
* Nous importons la fonction `+wish_details+` du module `+gwift.views+`
|
||||
* Champomy et cotillons! Nous avons une correspondance avec
|
||||
`+wishes/details/91827+`
|
||||
|
||||
Le module `+gwift.views+` qui se trouve dans le fichier
|
||||
`+gwift/views.py+` peut ressembler à ceci:
|
||||
|
||||
[source,Python]
|
||||
----
|
||||
# gwift/views.py
|
||||
|
||||
[...]
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def wishes_details(request: HttpRequest, wish_id: int) -> HttpResponse:
|
||||
context = {
|
||||
"user_name": "Bond,"
|
||||
"user_first_name": "James",
|
||||
"generated_at": datetime.now()
|
||||
}
|
||||
return render(
|
||||
request,
|
||||
"wish_details.html",
|
||||
context
|
||||
)
|
||||
----
|
||||
|
||||
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é.
|
||||
|
||||
[source,html]
|
||||
----
|
||||
<!-- fichier wish_details.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Page title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hi!</h1>
|
||||
<p>My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.</p>
|
||||
<p>This page was generated at {{ generated_at }}</p>
|
||||
</body>
|
||||
</html>
|
||||
----
|
||||
|
||||
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]
|
||||
----
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Page title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hi!</h1>
|
||||
<p>My name is Bond. James Bond.</p>
|
||||
<p>This page was generated at 2027-03-19 19:47:38</p>
|
||||
</body>
|
||||
</html>
|
||||
----
|
||||
|
||||
.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
|
||||
|
||||
Quel que soit le framework de tests choisi (django-tests, pytest,
|
||||
unittest, …), la couverture de code est une analyse qui donne un
|
||||
pourcentage lié à la quantité de code couvert par les tests. Il ne
|
||||
s’agit pas de vérifier que le code est bien testé, mais de vérifier
|
||||
quelle partie du code est testée. Le paquet coverage se charge d’évaluer
|
||||
le pourcentage de code couvert par les tests. Avec pytest, il convient
|
||||
d’utiliser le paquet 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 paquet django-coverage-plugin.
|
||||
Ajoutez-le dans le fichier requirements/base.txt, et lancez une
|
||||
couverture de code grâce à la commande coverage. La configuration peut
|
||||
se faire dans un fichier .coveragerc que vous placerez à la racine de
|
||||
votre projet, et qui sera lu lors de l’exécution.
|
||||
|
||||
....
|
||||
# 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
|
||||
....
|
||||
|
||||
Nous pouvons à présent jouer au jeu de la couverture, qui consiste à
|
||||
augmenter ou égaliser la couverture existante à chaque nouvelle
|
||||
fonctionnalité ajoutée ou bug corrigé. De cette manière, sans arriver à
|
||||
une couverture de 100%, chaque modification du code améliorera la base
|
||||
existante. footnote:[cf. Two Scoops of Django]. Suivant l’outil
|
||||
d’intégration continue que vous utiliserez, cette évolution pourra être
|
||||
affichée à chaque demande de fusion, et pourra être considérée comme un
|
||||
indicateur de qualité.
|
||||
|
||||
....
|
||||
$ coverage run --source "." manage.py test
|
||||
|
||||
$ coverage report
|
||||
|
||||
Name Stmts Miss Cover
|
||||
---------------------------------------------
|
||||
gwift\gwift\__init__.py 0 0 100%
|
||||
gwift\gwift\settings.py 17 0 100%
|
||||
gwift\gwift\urls.py 5 5 0%
|
||||
gwift\gwift\wsgi.py 4 4 0%
|
||||
gwift\manage.py 6 0 100%
|
||||
gwift\wish\__init__.py 0 0 100%
|
||||
gwift\wish\admin.py 1 0 100%
|
||||
gwift\wish\models.py 49 16 67%
|
||||
gwift\wish\tests.py 1 1 0%
|
||||
gwift\wish\views.py 6 6 0%
|
||||
---------------------------------------------
|
||||
TOTAL 89 32 64%
|
||||
----
|
||||
$ coverage html
|
||||
....
|
||||
|
||||
==== 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._
|
||||
____
|
||||
|
||||
=== Conclusions
|
||||
|
||||
Comme nous l’avons vu dans la première partie, Django est un framework
|
||||
complet, intégrant tous les mécanismes nécessaires à la bonne évolution
|
||||
d’une application. Il est possible de démarrer petit, et de suivre
|
||||
l’évolution des besoins en fonction de la charge estimée ou ressentie,
|
||||
d’ajouter un mécanisme de mise en cache, des logiciels de suivi, …
|
File diff suppressed because it is too large
Load Diff
|
@ -5,3 +5,72 @@
|
|||
|
||||
Lorsque nous démarrons une nouvelle application, la toute première version est généralement la partie la plus simple à mettre en place : elle ne nécessite pratiquement aucune maintenance ou considération pour les futurs désidérata de vos clients.
|
||||
Les tests unitaires et d'intégration *vous* place comme client et consommateur de votre propre travail.
|
||||
|
||||
=== Tests unitaires
|
||||
|
||||
Comme tout bon *langage de programmation moderne* qui se respecte,
|
||||
Python embarque tout un environnement facilitant le lancement de tests;
|
||||
Une bonne pratique (parfois discutée) consiste cependant à switcher vers
|
||||
`+pytest+`, qui présente quelques avantages par rapport au module
|
||||
`+unittest+`:
|
||||
|
||||
* Une syntaxe plus concise (au prix de
|
||||
https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html[quelques
|
||||
conventions], même si elles restent configurables): un test est une
|
||||
fonction, et ne doit pas obligatoirement faire partie d’une classe
|
||||
héritant de `+TestCase+` - la seule nécessité étant que cette fonction
|
||||
fasse partie d’un module commençant ou finissant par "test"
|
||||
(`+test_example.py+` ou `+example_test.py+`).
|
||||
* Une compatibilité avec du code Python "classique" - vous ne devrez
|
||||
donc retenir qu’un seul ensemble de commandes ;-)
|
||||
* 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
|
||||
biesse; on est sur la partie théorique ici):
|
||||
|
||||
[source,Python]
|
||||
----
|
||||
def test_add():
|
||||
assert 0 + 0 == "La tête à Toto"
|
||||
----
|
||||
|
||||
Forcément, cela va planter. Pour nous en assurer (dès fois que quelqu’un
|
||||
en doute), il nous suffit de démarrer la commande `+pytest+`:
|
||||
|
||||
....
|
||||
pytest
|
||||
================= test session starts =================
|
||||
platform ...
|
||||
rootdir: ...
|
||||
plugins: django-4.1.0
|
||||
collected 1 item
|
||||
gwift\test_models.py F
|
||||
[100%]
|
||||
================= FAILURES =================
|
||||
_________________ test_basic_add _________________
|
||||
def test_basic_add():
|
||||
> assert 0 + 0 == "La tête à Toto"
|
||||
E AssertionError: assert (0 + 0) == 'La tête à Toto'
|
||||
tests.py:2: AssertionError
|
||||
================= short test summary info =================
|
||||
FAILED tests.py::test_basic_add - AssertionError: assert (0 + 0) == 'La tête à Toto'
|
||||
================= 1 failed in 0.10s =================
|
||||
....
|
||||
|
||||
==== Couverture de code
|
||||
|
||||
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.
|
|
@ -51,8 +51,12 @@ include::book/environment/maintenability.adoc[]
|
|||
|
||||
include::book/environment/elements-of-architecture.adoc[]
|
||||
|
||||
include::book/environment/tests.adoc[]
|
||||
|
||||
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[]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
= Reporting
|
||||
== Reporting
|
||||
|
||||
== Status page
|
||||
=== Status page
|
||||
|
||||
https://www.atlassian.com/software/statuspage/pricing
|
||||
|
||||
|
|
Loading…
Reference in New Issue