migrate rst structure to adoc

This commit is contained in:
Fred Pauchet 2020-02-14 21:31:08 +01:00
parent 17e88d5cbb
commit 3b60990c13
34 changed files with 3349 additions and 748 deletions

View File

@ -1,6 +1,6 @@
=== Snippets utiles (et forcément dispensables)
== Snippets utiles (et forcément dispensables)
==== Récupération du dernier tag Git en Python
=== Récupération du dernier tag Git en Python
L'idée ici est simplement de pouvoir afficher le numéro de version ou le hash d'exécution du code, sans avoir à se connecter au dépôt. Cela apporte une certaine transparence, *sous réserve que le code soit géré par Git*. Si vous suivez scrupuleusement les 12 facteurs, la version de l'application déployée n'est plus sensée conserver un lien avec votre dépôt d'origine... Si vous déployez votre code en utilisant un `git fetch` puis un `git checkout <tag_name>`, le morceau de code ci-dessous pourra vous intéresser :-)

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,16 +1,12 @@
====================
Intégration continue
====================
== Intégration continue avec Jenkins
Le but de l'intégration est continue est de nous permettre de tester automatiquement notre développement chaque fois que le code est mis à jour et ainsi éviter les régressions.
Ceci nécessite de mettre à jour régulièrement les tests et d'utiliser un serveur d'intégration. Dans notre cas, nous allons utiliser jenkins.
Nous considérons aussi que le code est hébergé sur gitlab (par exemple celui de `framasof <https://git.framasoft.org/>`_)
Nous considérons aussi que le code est hébergé sur Gitlab.
***********************
Installation de jenkins
***********************
=== Installation de Jenkins
Jenkins fournit des paquets d'installation pour presque tous les systèmes d'exploitation sur leur site: `https://jenkins-ci.org/ <https://jenkins-ci.org/>`_.
@ -19,9 +15,7 @@ Par exemple, dans le cas de debian, il suffit de suivre les instructions sur `ht
Comme nous utilisons git, il faut veiller à activer le plugin correspondant: `Git plugin <https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin>`_.
Ce dernier peut directement être installé depuis le panneau de gestion des plugins de jenkins.
********************
Création d'un projet
********************
=== Création d'un projet
Depuis la page principale de jenkins, on crée un nouveau projet en cliquant sur *Nouveau Item* et on donne un nom à notre nouveau projet:
@ -40,9 +34,8 @@ Finalement, on écrit le petit script permettant de lancer le build:
Et on sauve le tout.
*********************
Lien gitlab - jenkins
*********************
=== Lien gitlab - jenkins
Pour que le build du projet que nous avons créé dans jenkins soit exécuté automatiquement, il est nécessaire d'autoriser le lancement du build via une url sur jenkins, cette dernière étant appelée depuis gitlab.

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,4 +1,4 @@
### Connexion et préparation du serveur
== Déploiement sur CentOS
[source,bash]
----
@ -8,9 +8,9 @@ groupadd --system gunicorn_sockets
useradd --system --gid webapps --shell /bin/bash --home /home/medplan medplan
mkdir -p /home/medplan
chown medplan:webapps /home/medplan
---
----
### Installation des dépendances systèmes
=== Installation des dépendances systèmes
[source,bash]
----
@ -22,7 +22,7 @@ wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sq
sudo yum install sqlite-3.8.11-1.fc21.x86_64.rpm sqlite-devel-3.8.11-1.fc21.x86_64.rpm -y
----
### Préparation de l'environnement utilisateur
=== Préparation de l'environnement utilisateur
[source,bash]
----
@ -55,7 +55,7 @@ cd webapps/medplan
gunicorn config.wsgi:application --bind localhost:3000 --settings=config.settings_production
----
### Configuration de l'application
=== Configuration de l'application
[source,bash]
----
@ -64,22 +64,30 @@ ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/medplan/static
----
### Création des répertoires de logs
=== Création des répertoires de logs
[source,text]
----
mkdir -p /var/www/medplan/static
----
### Création du répertoire pour le socket
=== Création du répertoire pour le socket
Dans le fichier /etc/tmpfiles.d/medplan.conf:
Dans le fichier `/etc/tmpfiles.d/medplan.conf`:
[source,text]
----
D /var/run/webapps 0775 medplan gunicorn_sockets -
----
Suivi de la création par systemd :
systemd-tmpfiles --create
### Gunicorn
[source,text]
----
systemd-tmpfiles --create
----
=== Gunicorn
[source,bash]
----
@ -111,14 +119,14 @@ exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--log-file=-
----
### Supervision
=== Supervision
[source,bash]
----
yum install supervisor -y
----
On crée ensuite le fichier /etc/supervisord.d/medplan.ini:
On crée ensuite le fichier `/etc/supervisord.d/medplan.ini`:
[source,bash]
----
@ -130,11 +138,15 @@ autostart=true
autorestart=unexpected
redirect_stdout=true
redirect_stderr=true
----
Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu'il tourne correctement:
[source,bash]
----
mkdir /var/log/medplan
chown medplan:nagios /var/log/medplan
systemctl enable supervisord
systemctl start supervisord.service
systemctl status supervisord.service
@ -154,7 +166,7 @@ systemctl status supervisord.service
ls /var/run/webapps/
----
### Ouverture des ports
=== Ouverture des ports
[source,text]
----
@ -163,7 +175,7 @@ firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload
----
### Installation d'Nginx
=== Installation d'Nginx
[source]
----
@ -171,7 +183,7 @@ yum install nginx -y
usermod -a -G gunicorn_sockets nginx
----
On configure ensuite le fichier /etc/nginx/conf.d/medplan.conf:
On configure ensuite le fichier `/etc/nginx/conf.d/medplan.conf`:
----
upstream medplan_app {
@ -213,9 +225,9 @@ server {
}
----
### Configuration des sauvegardes
=== Configuration des sauvegardes
Les sauvegardes ont été configurées avec borg: yum install borgbackup.
Les sauvegardes ont été configurées avec borg: `yum install borgbackup`.
C'est l'utilisateur medplan qui s'en occupe.
@ -230,4 +242,4 @@ Et dans le fichier crontab :
----
0 23 * * * /home/medplan/bin/backup.sh
----
----

View File

@ -1,4 +1,4 @@
== Déploiement
== Un peu de théorie... Les principales étapes
On va déjà parler de déploiement. Si vous avez suivi les étapes jusqu'ici, vous devriez à peine disposer d'un espace de travail proprement configuré, d'un modèle relativement basique et d'une configuration avec une base de données simpliste. En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce que vous souhaitez au final.

0
adoc/gwift/specs.adoc Normal file
View File

View File

@ -1,4 +1,4 @@
= Deep dive into Django
= Minor swing with Django
Cédric Declerfayt <jaguarondi27@gmail.com>; Fred Pauchet <fred@grimbox.be>
:doctype: book
:toc:
@ -13,37 +13,75 @@ L'idée du texte ci-dessous est de jeter les bases d'un bon développement, en s
Ces idées ne s'appliquent pas uniquement à Django et à son cadre de travail, ni même au langage Python. Juste que ces deux bidules sont de bons candidats et que le cadre de travail est bien défini et suffisamment flexible.
Django se présente comme un `Framework Web pour perfectionnistes ayant des deadlines <https://www.djangoproject.com/>`_.
Django suit quelques principes <https://docs.djangoproject.com/en/dev/misc/design-philosophies/>:
* Faible couplage et forte cohésion, pour que chaque composant ait son indépendance.
* Moins de code, plus de fonctionnalités.
* `Don't repeat yourself <https://fr.wikipedia.org/wiki/Sec>`_: ne pas se répéter!
* Rapidité du développement (après une courbe d'apprentissage relativement ardue, malgré tout)
Mis côté à côté, l'application de ces principes permet une meilleure stabilité du projet. Dans la suite de ce chapitre, on verra comment configurer l'environnement, comment installer Django de manière isolée et comment démarrer un nouveau projet. On verra comment gérer correctement les dépendances, les versions et comment applique un score sur note code.
Finalement, on verra aussique la configuration proposée par défaut par le framework n'est pas idéale pour la majorité des cas.
Pour cela, on présentera différents outils (mypy, flake8, black, ...), la rédaction de tests unitaires et d'intégration pour limiter les régressions, les règles de nomenclature et de contrôle du contenu, ainsi que les bonnes étapes à suivre pour arriver à un déploiement rapide et fonctionnel avec peu d'efforts.
Et tout ça à un seul et même endroit. Oui. :-)
Bonne lecture.
== Environnement de travail
= Environnement de travail
Avant de démarrer le développement, il est nécessaire de passer un peu de temps sur la configuration de l'environnement.
Les morceaux de code seront développés pour Python3.4+ et Django 1.8+. Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure.
**Remarque** : les commandes qui seront exécutés dans ce livre le seront depuis un shell sous GNU/Linux. Certaines devront donc être adaptées si vous êtes dans un autre environnemnet.
include::toolchain/12-factors.adoc[]
include::toolchain/venvs.adoc[]
include::toolchain/django.adoc[]
include::toolchain/maintainable-applications.adoc[]
include::toolchain/tools.adoc[]
include::toolchain/external_tools.adoc[]
== Déploiement
= Déploiement
Et sécurisation du serveur.
include::deploy/index.adoc[]
include::deploy/centos.adoc[]
== Modélisation
= Modélisation
Dans ce chapitre, on va parler de plusieurs concepts utiles au développement rapide d'une application. On parlera de modélisation, de migrations, d'administration auto-générée.
include::models/models.adoc[]
include::models/querysets.adoc[]
include::models/forms.adoc[]
include::models/migrations.adoc[]
include::models/admin.adoc[]
== Go Live !
Et supervision.
= Go Live !
== En bonus
== Supervision des logs
include::tools/code-snippets.adoc[]
== feedbacks utilisateurs
= En bonus
include::bonus/code-snippets.adoc[]

2687
adoc/main.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,4 @@
== Modélisation et conception avancée
Dans ce chapitre, on va parler de plusieurs concepts utiles au développement rapide d'une application. On parlera de modélisation, de migrations, d'administration auto-générée.
=== Modélisation
On va aborder la modélisation des objets en elle-même, qui s'apparente à la conception de la base de données.
Django utilise un modèle https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[ORM] - c'est-à-dire que chaque objet peut s'apparenter à une table SQL, mais en ajoutant une couche propre au paradigme orienté objet. Il sera ainsi possible de définir facilement des notions d'héritage (tout en restant dans une forme d'héritage simple), la possibilité d'utiliser des propriétés spécifiques, des classes intermédiaires, ...
L'avantage de tout ceci est que tout reste au niveau du code. Si l'on revient sur la méthodologie des douze facteurs, ce point concerne principalement la minimisation de la divergence entre les environnements d'exécution. Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande, dans la mesure où *tout est embarqué au niveau du code*.
Assez de blabla, on démarre !
=== Migrations
Les migrations (comprendre les "migrations du schéma de base de données") sont intimement liées à la représentation d'un contexte fonctionnel. L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre.
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. 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.
=== Administration
== Administration
Woké. On va commencer par la *partie à ne _surtout_ (__surtout__ !!) pas faire en premier dans un projet Django*. Mais on va la faire quand même. La raison principale est que cette partie est tellement puissante et performante, qu'elle pourrait laisser penser qu'il est possible de réaliser une application complète rien qu'en configurant l'administration.
@ -35,3 +12,39 @@ Elle doit rester dans les mains d'administrateurs ou de gestionnaires, et dans l
Une bonne idée consiste à développer l'administration dans un premier temps, en *gardant en tête qu'il sera nécessaire de développer des concepts spécifiques*. Dans cet objectif, l'administration est un outil exceptionel, qui permet de valider un modèle, de créer des objets rapidement et de valider les liens qui existent entre eux. C'est un excellent outil de prototypage et de preuve de concept.
=== Quelques conseils
. Surchargez la méthode `__str__(self)` pour chaque classe que vous aurez définie dans le modèle. Cela permettra de construire une représentation textuelle qui représentera l'instance de votre classe. Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l'on manipule. En plus, cette méthode est également appelée lorsque l'administration historisera une action (et comme cette étape sera inaltérable, autant qu'elle soit fixée dans le début).
. La méthode `get_absolute_url(self)` retourne l'URL à laquelle on peut accéder pour obtenir les détails d'une instance. Par exemple:
[source,python]
----
def get_absolute_url(self):
return reverse('myapp.views.details', args=[self.id])
----
. Les attributs `Meta`:
[source,python]
----
class Meta:
ordering = ['-field1', 'field2']
verbose_name = 'my class in singular'
verbose_name_plural = 'my class when is in a list!'
----
. Le titre:
* Soit en modifiant le template de l'administration
* Soit en ajoutant l'assignation suivante dans le fichier `urls.py`: `admin.site.site_header = "SuperBook Secret Area`.
. Prefetch
https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64
https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614
En gros, le problème de l'admin est que si on fait des requêtes imbriquées, on va flinguer l'application et le chargement de la page.
La solution consiste à utiliser la propriété `list_select_related` de la classe d'Admin, afin d'appliquer une jointure par défaut et
et gagner en performances.

106
adoc/models/forms.adoc Normal file
View File

@ -0,0 +1,106 @@
== Forms
Ou comment valider proprement des données entrantes.
NOTE: intégrer le dessin XKCD avec Little Bobby Table sur l'assainissement des données en entrée :-p
Quand on parle de `forms`, on ne parle pas uniquement de formulaires Web. On pourrait considérer qu'il s'agit de leur objectif principal, mais on peut également voir un peu plus loin: on peut en fait voir les `forms` comme le point d'entrée pour chaque donnée arrivant dans notre application: il s'agit en quelque sorte d'un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle.
L'exemple le plus simple est un fichier `.csv`: la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle.
Mauvaise idée.
Les données fournies par un utilisateur **doivent** **toujours** être validées avant introduction dans la base de données. Notre base de données étant accessible ici par l'ORM, la solution consiste à introduire une couche supplémentaire de validation.
Le flux à suivre est le suivant:
. Création d'une instance grâce à un dictionnaire
. Validation des données et des informations reçues
. Traitement, si la validation a réussi.
Ils jouent également plusieurs rôles:
. Validation des données, en plus de celles déjà définies au niveau du modèle
. Contrôle sur le rendu à appliquer aux champs
Ils agissent come une glue entre l'utilisateur et la modélisation de vos structures de données.
=== Dépendance avec le modèle
Un **form** peut dépendre d'une autre classe Django. Pour cela, il suffit de fixer l'attribut `model` au niveau de la `class Meta` dans la définition.
[source,python]
----
from django import forms
from wish.models import Wishlist
class WishlistCreateForm(forms.ModelForm):
class Meta:
model = Wishlist
fields = ('name', 'description')
----
De cette manière, 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.
=== Rendu et affichage
Le formulaire permet également de contrôler le rendu qui sera appliqué lors de la génération de la page. Si les champs dépendent du modèle sur lequel se base le formulaire, ces widgets doivent être initialisés dans l'attribut `Meta`. Sinon, ils peuvent l'être directement au niveau du champ.
[source,python]
----
from django import forms
from datetime import date
from .models import Accident
class AccidentForm(forms.ModelForm):
class Meta:
model = Accident
fields = ('gymnast', 'educative', 'date', 'information')
widgets = {
'date' : forms.TextInput(
attrs={
'class' : 'form-control',
'data-provide' : 'datepicker',
'data-date-format' : 'dd/mm/yyyy',
'placeholder' : date.today().strftime("%d/%m/%Y")
}),
'information' : forms.Textarea(
attrs={
'class' : 'form-control',
'placeholder' : 'Context (why, where, ...)'
})
----
=== Squelette par défaut
On a d'un côté le {{ form.as_p }} ou {{ form.as_table }}, mais il y a beaucoup mieux que ça ;-) Voir les templates de Vitor.
=== Crispy-forms
Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela permet de valider des données reçues en entrée et d'afficher (très) facilement des formulaires à compléter par l'utilisateur.
Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près, ... Tout ça demande énormément de temps. Et c'est là qu'intervient `Django-Crispy-Forms <http://django-crispy-forms.readthedocs.io/en/latest/>`_. 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.
(c/c depuis le lien ci-dessous)
Pour chaque champ, crispy-forms va :
* utiliser le `verbose_name` comme label.
* vérifier les paramètres `blank` et `null` pour savoir si le champ est obligatoire.
* utiliser le type de champ pour définir le type de la balise `<input>`.
* récupérer les valeurs du paramètre `choices` (si présent) pour la balise `<select>`.
http://dotmobo.github.io/django-crispy-forms.html
== Validation des données
NOTE: parler ici des méthodes `clean`.
== En conclusion
. Toute donnée entrée par l'utilisateur **doit** passer par une instance de `form`.
. euh ?

View File

@ -0,0 +1,6 @@
== Migrations
Les migrations (comprendre les "migrations du schéma de base de données") sont intimement liées à la représentation d'un contexte fonctionnel. L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre.
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. 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.

9
adoc/models/models.adoc Normal file
View File

@ -0,0 +1,9 @@
== Modélisation
On va aborder la modélisation des objets en elle-même, qui s'apparente à la conception de la base de données.
Django utilise un modèle https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[ORM] - c'est-à-dire que chaque objet peut s'apparenter à une table SQL, mais en ajoutant une couche propre au paradigme orienté objet. Il sera ainsi possible de définir facilement des notions d'héritage (tout en restant dans une forme d'héritage simple), la possibilité d'utiliser des propriétés spécifiques, des classes intermédiaires, ...
L'avantage de tout ceci est que tout reste au niveau du code. Si l'on revient sur la méthodologie des douze facteurs, ce point concerne principalement la minimisation de la divergence entre les environnements d'exécution. Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande, dans la mesure où *tout est embarqué au niveau du code*.
Assez de blabla, on démarre !

View File

@ -1,5 +1,4 @@
Queryset & managers
===================
== Queryset & managers
L'ORM de Django propose par défaut deux objets hyper importants:
@ -8,4 +7,4 @@ L'ORM de Django propose par défaut deux objets hyper importants:
d'autres filtres à des filtres existants.
En plus de cela, il faut bien tenir compte des propriétés `Meta` de la classe: si elle contient déjà un ordre par défaut, celui-ci
sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe.
sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe.

View File

@ -1,10 +1,10 @@
=== Méthodologie de travail
== Méthodologie de travail
Pour la méthode de travail et de développement, on va se baser sur les https://12factor.net/fr/[The Twelve-factor App] - ou plus simplement les *12 facteurs*.
L'idée derrière cette méthode consiste à pousser les concepts suivants (repris grossièrement de la https://12factor.net/fr/[page d'introduction] :
. Faciliter la mise en place de phases d'automatisation; plus simplement, faciliter les mises à jour applicatives, simplifier la gestion de l'hôte, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'https://en.wikipedia.org/wiki/Continuous_integration[intégration continue]/https://en.wikipedia.org/wiki/Continuous_deployment[déploiement continu]
. Faciliter la mise en place de phases d'automatisation; plus simplement, faciliter les mises à jour applicatives, simplifier la gestion de l'hôte, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'https://en.wikipedia.org/wiki/Continuous_integration[intégration continue]/link:https://en.wikipedia.org/wiki/Continuous_deployment[déploiement continu]
. Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet.
. Faciliter
. Augmenter l'agilité générale du projet, en permettant une meilleure évolutivité architecturale et une meilleure mise à l'échelle - _Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on n'en parle plus ;-)_.
@ -36,4 +36,4 @@ NOTE: pensez à retravailler la partie ci-dessous; la version anglophone semble
|Configuration dans l'environnement| Fichiers .ENV| Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, on ne doit trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code.
|Services externes = ressources locales| Fichiers .ENV| Chaque ressource doit pouvoir être interchangeable avec une autre, sans modification du code source. La solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement via des variables d'environnement.
|Bien séparer les étapes de construction des étapes de mise à disposition| Capistrano, Gitea, un serveur d'artefacts, ...| L'idée est de pouvoir récupérer une version spécifique du code, sans que celle-ci ne puisse avoir été modifiée. Git permet bien de gérer des versions (au travers des tags), mais ces éléments peuvent sans doute être modifiés directement au travers de l'historique.
|
|===

View File

@ -1,6 +1,4 @@
*******************
L'espace de travail
*******************
== Django
Comme on l'a vu ci-dessus, ``django-admin`` permet de créer un nouveau projet. On fait ici une distinction entre un **projet** et une **application**:
@ -11,8 +9,7 @@ Pour ``gwift``, on va notamment avoir une application pour la gestion des listes
On voit bien ici le principe de **contexte**: l'application viendra avec son modèle, ses tests, ses vues et son paramétrage. Elle pourra éventuellement être réutilisée dans un autre projet. C'est en ça que consistent les `paquets Django <https://www.djangopackages.com/>`_ déjà disponibles: ce sont simplement de petites applications empaquetées pour être réutilisées (eg. `Django-Rest-Framework <https://github.com/tomchristie/django-rest-framework>`_, `Django-Debug-Toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_, ...).
Gestion
=======
=== Gestion
Comme expliqué un peu plus haut, le fichier ``manage.py`` 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:
@ -29,23 +26,24 @@ La liste complète peut être affichée avec ``manage.py help``. Vous remarquere
Nous verrons plus tard comment ajouter de nouvelles commandes.
Structure d'une application
===========================
=== Structure d'une application
Maintenant que l'on a vu à quoi servait ``manage.py``, on peut créer notre nouvelle application grâce à la commande ``manage.py startapp <label>``.
Cette application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir. Essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc ``wish``. C'est parti pour ``manage.py startapp wish``!
.. code-block:: shell
$ python manage.py startapp wish
[source,bash]
----
$ python manage.py startapp wish
----
Résultat? Django nous a créé un répertoire ``wish``, dans lequel on trouve les fichiers suivants:
.. code-block:: shell
$ ls -l wish
admin.py __init__.py migrations models.py tests.py views.py
[source,bash]
----
$ ls -l wish
admin.py __init__.py migrations models.py tests.py views.py
----
En résumé, chaque fichier a la fonction suivante:
@ -54,4 +52,13 @@ En résumé, chaque fichier a la fonction suivante:
* ``migrations/``, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* ``models.py`` pour représenter et structurer nos données.
* ``tests.py`` pour les tests unitaires.
=== Tests unitaires
Plein de trucs à compléter ici ;-) Est-ce qu'on passe par pytest ou par le framework intégré ? Quels sont les avantages de l'un % à l'autre ?
* ``views.py`` pour définir ce que nous pouvons faire avec nos données.

View File

@ -1,6 +1,4 @@
=============
Documentation
=============
== Documentation
Documentation: be obsessed!

View File

@ -1,4 +1,12 @@
=== graphviz
== Git
NOTE: insérer ici une description de Gitflow + quelques exemples types création, ajout, suppression, historique, branches, ... et quelques schémas qui-vont-bien.
Il existe plusiseurs outils permettant de gérer les versions du code, dont les plus connus sont `git <https://git-scm.com/>`_ et `mercurial <https://www.mercurial-scm.org/>`_.
Dans notre cas, nous utilisons git et hebergons le code et le livre directement sur le gitlab de `framasoft <https://git.framasoft.org/>`_
== graphviz
En utilisant django_extensions (! bien suivre les étapes d'installation !).

View File

@ -0,0 +1,36 @@
== Construire des applications maintenables
Pour cette section, je me base d'un résumé de l'ebook **Building Maintenable Software** disponible chez `O'Reilly <http://shop.oreilly.com/product/0636920049555.do`_ qui vaut clairement le détour pour poser les bases d'un projet.
Ce livre répartit un ensemble de conseils parmi quatre niveaux de composants:
* Les méthodes et fonctions
* Les classes
* Les composants
* Et de manière plus générale.
=== Au niveau des méthodes et fonctions
* Gardez vos méthodes/fonctions courtes. Pas plus de 15 lignes, en comptant les commentaires. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9% de plus de 60 lignes; pas plus de 22.3% de plus de 30 lignes, au plus 43.7% de plus de 15 lignes et au moins 56.3% en dessous de 15 lignes). Oui, c'est dur à tenir, mais faisable.
* Conserver une complexité de McCabe en dessous de 5, c'est-à-dire avec quatre branches au maximum. A nouveau, si on a une méthode avec une complexité cyclomatique de 15, la séparer en 3 fonctions avec une complexité de 5 conservera globalement le nombre 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
* N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc., c'est juste mal: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées. C'est aussi une forme de régression.
* Conservez de petites interfaces. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe, plus facile à tester.
=== Au niveau des classes
* Privilégiez un couplage faible entre vos classes. Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences. L'implémentation du service ``UserNotificationsService`` ne doit pas forcément se trouver embarqué dans une classe ``UserService``. De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction. La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée.
=== Au niveau des composants
* Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants également. Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'on ne puisse contacter trop facilement des couches séparées de l'architecture. Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "juste parce qu'elle en a la possibilité". Vous pourrez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que `IFileTransfer` avec ses méthodes `put` et `get`, et non pas les détails d'implémentation complet d'une classe `FtpFileTransfer` ou `SshFileTransfer`).
* Conserver un bon balancement au niveau des composants: évitez qu'un composant **A** ne soit un énorme mastodonte, alors que le composant juste à côté n'est capable que d'une action. De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités plus faciles à gérer. Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que ces composants soit approximativement de même taille.
=== De manière plus générale
* Conserver une densité de code faible: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisant du refactoring (intensif?), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux. Mais surtout en évitant de copier/coller bêtement du code existant.
* Automatiser les tests, ajouter un environnement d'intégration continue dès le début du projet et vérifier par des outils les points ci-dessus.
=== En pratique
Par rapport aux points repris ci-dessus, l'environnement Python et le framework Django proposent un ensemble d'outils intégrés qui permettent de répondre à chaque point. Avant d'aller plus loin, donc, un petit point sur les conventions, les tests (unitaires, orientés comportement, basés sur la documentation, ...), la gestion de version du code et sur la documentation. Plus que dans tout langage compilé, ceux-ci sont pratiquement obligatoires. Vous pourrez les voir comme une perte de temps dans un premier temps, mais nous vous promettons qu'ils vous en feront gagner par la suite.

View File

@ -1,10 +1,207 @@
=== Chaîne d'outils
== Chaîne d'outils
Le langage Python fonctionne avec un système daméliorations basées sur des propositions: les PEP, ou “Python Enhancement Proposal”.
Celle qui va nous intéresser pour cette section est la https://www.python.org/dev/peps/pep-0008/[PEP 8 -- Style Guide for Python Code]. Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour lindentation, le nommage des variables et des classes, … En bref, elle décrit comment écrire du code proprement pour que dautres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
==== pep8, flake8, pylint
=== PEP8
Le langage Python fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le `Benevolent Dictator For Life <http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life>`_.
La PEP qui nous intéresse plus particulièrement pour la suite est la `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_, ou "Style Guide for Python Code". Elle spécifie des conventions d'organisation et de formatage de code Python, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, etc. En bref, elle décrit comment écrire du code proprement pour que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (``.``, le nom d'un répertoire, le nom d'un fichier ``.py``, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options ``--statistics -qq``.
.. code-block:: shell
$ pep8 . --statistics -qq
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez ``pyflakes``: il analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
Finalement, la solution qui couvre ces deux domaines existe et s'intitule `flake8 <https://github.com/PyCQA/flake8>`_. Sur base la même interface que ``pep8``, vous aurez en plus tous les avantages liés à ``pyflakes`` concernant votre code source.
==== PEP257
.. todo:: à remplir avec ``pydocstyle``.
==== Tests
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe ``TestCase`` depuis le package ``django.test``:
.. code-block:: python
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
==== Couverture de code
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet `coverage <https://pypi.python.org/pypi/coverage/>`_, qui se charge d'évaluer le pourcentage de code couvert par les tests. 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.
.. code-block:: shell
# requirements/base.text
[...]
coverage
django_coverage_plugin
.. code-block:: shell
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
.. todo:: le bloc ci-dessous est à revoir pour isoler la configuration.
.. code-block:: shell
$ 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
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. 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:
.. code-block:: shell
# 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."
==== Complexité de McCabe
La `complexité cyclomatique <https://fr.wikipedia.org/wiki/Nombre_cyclomatique>`_ (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:
.. code-block:: python
if True == True:
pass # never happens
# continue ...
La condition existe, mais on ne passera jamais dedans. A l'inverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:
.. code-block:: python
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusqu'à arriver à l'impression à l'écran et à la valeur de retour. La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. Si on rencontre une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour s'assurer que la couverture est complète. On devrait donc trouver:
1. Un test pour entrer (ou non) dans la condition ``a == b``
2. Un test pour entrer (ou non) dans la condition ``b == c``
3. Un test pour entrer (ou non) dans la condition ``c == d``
4. Un test pour entrer (ou non) dans la condition ``d == e``
5. Et s'assurer que n'importe quel autre cas retournera la valeur ``None``.
On a donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
.. note::
Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale
A nouveau, un greffon pour ``flake8`` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre ``--max-complexity``. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
==== Documentation
Il existe plusieurs manières de générer la documentation d'un projet. Les plus connues sont `Sphinx <http://sphinx-doc.org/>`_ et `MkDocs <http://www.mkdocs.org/>`_. Le premier a l'avantage d'être plus reconnu dans la communauté Python que l'autre, de pouvoir *parser* le code pour en extraire la documentation et de pouvoir lancer des `tests orientés documentation <https://duckduckgo.com/?q=documentation+driven+development&t=ffsb>`_. A contrario, votre syntaxe devra respecter `ReStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans son résultat.
Dans l'immédiat, nous nous contenterons d'avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/...). Dans la continuié de ``Flake8``, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.
.. note::
voir si il ne faudrait pas mieux passer par pydocstyle.
.. code-block:: shell
$ pip install flake8_docstrings
Lancez ensuite `flake8` avec la commande ``flake8 . --exclude="migrations"``. Sur notre projet (presque) vide, le résultat sera le suivant:
.. code-block:: shell
$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused
Bref, on le voit: nous n'avons que très peu de modules, et aucun d'eux n'est commenté.
En plus de cette méthode, Django permet également de rendre la documentation accessible depuis son interface d'administration.
=== pep8, flake8, pylint
Un outil existe et listera lensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour linstaller, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (., le nom dun répertoire, le nom dun fichier .py, ...).
@ -94,11 +291,13 @@ Pour reprendre la http://pylint.pycqa.org/en/latest/user_guide/message-control.h
* E errors, for probable bugs in the code
* F fatal, if an error occurred which prevented pylint from doing further* processing.
==== Black
PyLint est la version **++**, pour ceux qui veulent un code propre et sans bavure.
=== Black
==== pytest
=== pytest
==== mypy
=== mypy

View File

@ -1,4 +1,4 @@
=== Environnements virtuels
== Environnements virtuels
On va commencer avec la partie la moins funky, mais la plus utile, dans la vie d'un développeur: la gestion et l'isolation des dépendances.
@ -8,6 +8,146 @@ Il est tout à fait possible de s'en passer complètement dans le cadre de "peti
. Il est tout à fait envisagable que deux applications soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance.
> But it works on my machine! Then, we'll ship your machine.
> --A famous meme, And this is how Docker was born.
> -- A famous meme, And this is how Docker was born.
Nous allons utiliser le module `venv` afin de créer un `environnement virtuel <http://sametmax.com/les-environnement-virtuels-python-virtualenv-et-virtualenvwrapper/>`_ pour python.
NOTE: auparavant, on utilisait `virtualenvwrapper`. mais cela fait plusieurs années que je ne l'utilise plus. On l'intègre quand même ?
=== Création de l'environnement virtuel
Commencons par créer un environnement virtuel, afin d'y stocker les dépendances. Lancez `python3 -m venv gwift-env`.
[source,bash]
---
Intégrer les résultats de la création de l'environnement
---
Ceci créera l'arborescence de fichiers suivante, qui peut à nouveau être un peu différente en fonction du système d'exploitation:
.. code-block:: shell
$ ls .virtualenvs/gwift-env
bin include lib
Nous pouvons ensuite l'activer grâce à la commande `source gwift-env`.
[source,bash]
---
Intégrer les résultats de l'accès de l'environnement
---
Le *shell* signale que nous sommes bien dans l'environnement ``gwift-env`` en l'affichant avant le ``$``. Par la suite, nous considérerons que l'environnement virtuel est toujours activé, même si ``gwift-env`` n'est pas présent devant chaque ``$``.
A présent, tous les binaires de cet environnement prendront le pas sur les binaires du système. De la même manière, une variable ``PATH`` propre est définie et utilisée, afin que les librairies Python y soient stockées. C'est donc dans cet environnement virtuel que nous retrouverons le code source de Django, ainsi que des librairies externes pour Python une fois que nous les aurons installées.
Pour désactiver l'environnement virtuel, il suffira d'utiliser la commande ``deactivate``
=== Installation de Django et création du répertoire de travail
Comme l'environnement est activé, on peut à présent y installer Django. La librairie restera indépendante du reste du système, et ne polluera pas les autres projets.
C'est parti: ``pip install django``!
.. code-block:: shell
$ pip install django
Collecting django
Downloading Django-X.Y.Z
100% |################################|
Installing collected packages: django
Successfully installed django-X.Y.Z
Les commandes de création d'un nouveau site sont à présent disponibles, la principale étant ``django-admin startproject``. Par la suite, nous utiliserons ``manage.py``, qui constitue un *wrapper* autour de `django-admin`.
Pour démarrer notre projet, nous lançons donc ``django-admin startproject gwift``.
[source,bash]
----
$ django-admin startproject gwift
----
Cette action a pour effet de créer un nouveau dossier ``gwift``, dans lequel on trouve la structure suivante:
[source,bash]
----
$ tree gwift
gwift
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
----
C'est sans 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. Tant qu'on y est, nous pouvons rajouter les répertoires utiles à la gestion de notre projet, à savoir la documentation, les dépendances et le README:
[source,bash]
----
$ mkdir docs requirements
$ touch docs/README.md
----
[source,bash]
----
$ tree gwift
gwift
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
|-- docs/
|-- requirements/
|-- README
----
Chacun de ces fichiers sert à:
* ``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.
* ``wsgi.py`` contient la définition de l'interface `WSGI <https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface>`_, qui permettra à votre serveur Web (Nginx, Apache, ...) de faire un pont vers votre projet.
NOTE: déplacer la configuration dans un répertoire ``config`` à part.
=== Gestion des dépendances
Comme nous venons d'ajouter une dépendance à notre projet, nous allons 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 utilité:
* ``base.txt``
* ``dev.txt``
* ``staging.txt``
* ``production.txt``
Au début de chaque fichier, il suffira 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, ajoutez simplement ``django`` dans le fichier ``requirements/base.txt``.
[source,bash]
----
$ echo django >> requirements/base.txt
----
Par la suite, il vous faudra **absolument** spécifier les versions à utiliser: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets. Des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc. Si vous voulez être sûr et certain que le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances. Avec les mécanismes d'intégration continue et de tests unitaires, on verra plus loin comment se prémunir d'un changement inattendu.
=== Structure finale de l'environnement
Nous avons donc la strucutre finale pour notre environnement de travail:
[source,bash]
----
$ tree ~/gwift-project
gwift
├── docs
│   └── README.md
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
│   manage.py
└── requirements
└── base.txt
----

View File

@ -1,5 +0,0 @@
==============
Administration
==============
.. include:: admin/advices.rst

View File

@ -1,66 +0,0 @@
**************
Administration
**************
L'administration de Django est l'une des fonctionnalités qui saute le plus au yeux lorsqu'on présente le framework à un nouveau-venu: sur base du modèle défini dans les différentes applications, Django peut générer automagiquement les formulaires d'entrée de données, les pages de listing, des fonctions de recherche, ... Tout ça avec très très peu de lignes de code.
L'un des problèmes par contre est que cette partie d'administration n'est pas destinée aux utilisateurs: c'est plus pour un *super-utilisateur* ou un gestionnaire que pour un utilisateur *lambda*.
Lors de la définition d'une nouvelle classe, et puisque l'ORM se base sur Active Records, il peut être intéressant de définir une valeur pour les options suivantes:
Une représentation textuelle
============================
Surcharger la fonction ``def __str__(self)`` sur la classe permettra de retourner une chaîne de caractère qui représentera l'instance de la classe. Cette information est utilisée un peu partout dans le code, et donnera une meilleure idée de ce qu'on manipule.
En plus, c'est aussi ceci qui est appelé lorsque l'admin de Django historisera une action (et ceci sera inaltérable).
URL absolue
===========
La méthode `def get_absolute_url(self)` retourne l'URL à laquelle on peut envoyer une requête pour obtenir le maximum d'informations
concernant cette instance.
Par exemple:
.. code-block:: python
def get_absolute_url(self):
return reverse('myapp.views.details', args=[self.id])
Lorsqu'on en aura besoin, il suffira d'appeler cette méthode pour atterrir d'office sur la page de détails pour cette instance.
Meta
====
.. code-block:: python
class Meta:
ordering = ['-field1', 'field2']
verbose_name = 'my class in singular'
verbose_name_plural = 'my class when is in a list!'
Titre
=====
Le titre de l'administration peut être modifié de deux manières:
* Soit en modifiant le template de l'administration
* Soit en ajoutant l'assignation suivante dans le fichier `urls.py`: `admin.site.site_header = "SuperBook Secret Area`.
Greffons
========
L'interface d'administration est extensible dans une certaine mesure. Notamment utiliser ``django_extensions`` pour avoir les ForeignKey auto-complétées.
Prefetch
========
https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64
https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614
En gros, le problème de l'admin est que si on fait des requêtes imbriquées, on va flinguer l'application et le chargement de la page.
La solution consiste à utiliser la propriété `list_select_related` de la classe d'Admin, afin d'appliquer une jointure par défaut et
et gagner en performances.

View File

@ -1,21 +0,0 @@
*******
Annexes
*******
Django-Admin-honeypot
=====================
`django-admin-honeypot <https://django-admin-honeypot.readthedocs.io/en/latest/>`_ is a fake Django admin login screen to log and notify admins of attempted unauthorized access. This app was inspired by discussion in and around Paul McMillans security talk at DjangoCon 2011. Cette librairie est surtout utile si elle est couplée avec les *loggers*:
.. code-block:: python
# Taken directly from core Django code.
# Used here to illustrate an example only, so don't
# copy this into your project.
logger.warning("Forbidden (%s): %s",
REASON_NO_CSRF_COOKIE, request.path,
extra={
"status_code": 403,
"request": request,
}
)

View File

@ -1,24 +0,0 @@
========================================
Formulaires: comment valider des données
========================================
Quand on parle de ``forms``, on ne parle pas uniquement de formulaires Web. On pourrait considérer qu'il s'agit de leur objectif principal, mais on peut également voir un peu plus loin: on peut en fait voir les ``forms`` comme le point d'entrée pour chaque donnée arrivant dans notre application: il s'agit en quelque sorte d'un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle.
L'exemple le plus simple est un fichier ``.csv``: la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle. Mauvaise idée. Les données fournies par un utilisateur **doivent** **toujours** être validées avant introduction dans la base de données. Notre base de données étant accessible ici par l'ORM, la solution consiste à introduire une couche supplémentaire de validation.
Le flux à suivre est le suivant:
1. Création d'une instance grâce à un dictionnaire
2. Validation des informations reçues
3. Traitement, si la validation a réussi.
On définit un form comme une classe. Comme pour l'ORM, des attributs Meta peuvent être ajoutés, afin de récupérer automatiquement des
.. include:: forms/05-forms.rst
##########
Conclusion
##########
1. Toute donnée entrée par l'utilisateur **doit** passer par une instance de ``form``.
2.

View File

@ -1,53 +0,0 @@
*****
Forms
*****
Les forms ne s'appliquent pas uniquement pour les rendus de formulaires dans votre page web, mais pour toute information qui doit être entrée dans la base de données. Ils jouent en fait plusieurs rôles: validation des données (en plus de celles déjà définies au niveau du modèle), contrôle de la manière dans les champs doivent être visualisables, ... et agissent comme une colle entre l'utilisateur et la modélisation de vos tables et structures de données.
Dépendance avec le modèle
=========================
Un **form** peut dépendre d'une autre classe Django. Pour cela, il suffit de fixer l'attribut `model` au niveau de la `class Meta` dans la définition.
.. code-block:: python
from django import forms
from wish.models import Wishlist
class WishlistCreateForm(forms.ModelForm):
class Meta:
model = Wishlist
fields = ('name', 'description')
De cette manière, 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.
Contrôle du rendu
=================
Le formulaire permet également de contrôler le rendu qui sera appliqué lors de la génération de la page. Si les champs dépendent du modèle sur lequel se base le formulaire, ces widgets doivent être initialisés dans l'attribut ``Meta``. Sinon, ils peuvent l'être directement au niveau du champ.
.. code-block:: python
from django import forms
from datetime import date
from .models import Accident
class AccidentForm(forms.ModelForm):
class Meta:
model = Accident
fields = ('gymnast', 'educative', 'date', 'information')
widgets = {
'date' : forms.TextInput(
attrs={
'class' : 'form-control',
'data-provide' : 'datepicker',
'data-date-format' : 'dd/mm/yyyy',
'placeholder' : date.today().strftime("%d/%m/%Y")
}),
'information' : forms.Textarea(
attrs={
'class' : 'form-control',
'placeholder' : 'Context (why, where, ...)'
})

View File

@ -1,5 +0,0 @@
***********
Validations
***********
Ecrire ici tout le brol sur les méthodes ``clean``.

View File

@ -1,19 +0,0 @@
************
Crispy Forms
************
Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela permet de valider des données reçues en entrée et d'afficher (très) facilement des formulaires à compléter par l'utilisateur.
Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près, ... Tout ça demande énormément de temps. Et c'est là qu'intervient `Django-Crispy-Forms <http://django-crispy-forms.readthedocs.io/en/latest/>`_. 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.
(c/c depuis le lien ci-dessous)
Pour chaque champ, crispy-forms va :
* utiliser le ``verbose_name`` comme label.
* vérifier les paramètres ``blank`` et ``null`` pour savoir si le champ est obligatoire.
* utiliser le type de champ pour définir le type de la balise ``<input>``.
* récupérer les valeurs du paramètre ``choices`` (si présent) pour la balise ``<select>``.
http://dotmobo.github.io/django-crispy-forms.html

View File

@ -1,24 +0,0 @@
************
Introduction
************
Django se présente comme un `Framework Web pour perfectionnistes ayant des deadlines <https://www.djangoproject.com/>`_.
`Django suit quelques principes <https://docs.djangoproject.com/en/dev/misc/design-philosophies/>`_:
* Faible couplage et forte cohésion, pour que chaque composant ait son indépendance.
* Moins de code, plus de fonctionnalités.
* `Don't repeat yourself <https://fr.wikipedia.org/wiki/Sec>`_: ne pas se répéter!
* Rapidité du développement (après une courbe d'apprentissage relativement ardue, malgré tout)
Mis côté à côté, l'application de ces principespermet une meilleure stabilité du projet. Dans la suite de ce chapitre, on verra comment configurer l'environnement, comment installer Django de manière isolée et comment démarrer un nouveau projet. On verra comment gérer correctement les dépendances, les versions et comment applique un score sur note code.
Finalement, on verra aussique la configuration proposée par défaut par le framework n'est pas idéale pour la majorité des cas.
.. include:: intro/01-prerequisites.rst
.. include:: intro/02-create-django-app.rst
.. include:: intro/03-before-going-further.rst
.. include:: intro/04-docs.rst

View File

@ -1,172 +0,0 @@
=============
Environnement
=============
Avant de démarrer le développement, il est nécessaire de passer un peu de temps sur la configuration de l'environnement.
Les morceaux de code seront développés pour Python3.4+ et Django 1.8+. Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure.
**Remarque** : les commandes qui seront exécutés dans ce livre le seront depuis un shell sous GNU/Linux. Certaines devront donc être adaptées si vous êtes dans un autre environnemnet.
Virtualenv
----------
.. todo:: J'avais aussi lu que l'utilisation du module ``venv`` était plus conseillée pour Python3. A vérifier à l'occasion.
Nous allons utiliser ``virtualenv`` afin de créer un `environnement virtuel <http://sametmax.com/les-environnement-virtuels-python-virtualenv-et-virtualenvwrapper/>`_ pour python et ``virtualenvwrapper`` pour en faciliter la gestion, et les prérequis seront remplis.
Suivant votre distribution, il sera sans doute nécessaire d'éditer le fichier ``~/.bashrc`` (ou tout fichier lancé au démarrage de votre session) et de vérifier que la variable ``WORKON_HOME`` est bien définie et de faire un ``source`` sur le fichier ``virtualenvwrapper.sh`` (à adapter en fonction de votre distribution):
.. code-block:: shell
# ~/.bashrc
[...]
WORKON_HOME=~/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh
L'intérêt de ceci ? Ne pas devoir se soucier de l'emplacement des environnements virtuels, et pouvoir entièrement les découpler des sources sur lesquelles vous travaillez, en plus d'isoler le code, de créer un containeur pour les dépendances et d'être indépendant des librairies tierces déjà installées sur le système.
Création de l'environnement virtuel
-----------------------------------
Commencons par créer un environnement virtuel, afin d'y stocker les dépendances. Lancez ``mkvirtualenv gwift-env``.
.. code-block:: shell
$ mkvirtualenv -p python3 gwift-env
New python executable in gwift-env/bin/python3
Also creating executable in gwift-env/bin/python
Installing setuptools, pip...done.
Ceci créera l'arborescence de fichiers suivante, qui peut à nouveau être un peu différente en fonction du système d'exploitation:
.. code-block:: shell
$ ls .virtualenvs/gwift-env
bin include lib
Nous pouvons ensuite l'activer grâce à la commande ``workon gwift-env``.
.. code-block:: shell
$ workon gwift-env
(gwift-env)$ which python
(gwift-env)$ /home/jaguarondi/.virtualenv/gwift-env/bin/python
Le *shell* signale que nous sommes bien dans l'environnement ``gwift-env`` en l'affichant avant le ``$``. Par la suite, nous considérerons que l'environnement virtuel est toujours activé, même si ``gwift-env`` n'est pas présent devant chaque ``$``.
A présent, tous les binaires de cet environnement prendront le pas sur les binaires du système. De la même manière, une variable ``PATH`` propre est définie et utilisée, afin que les librairies Python y soient stockées. C'est donc dans cet environnement virtuel que nous retrouverons le code source de Django, ainsi que des librairies externes pour Python une fois que nous les aurons installées.
Pour désactiver l'environnement virtuel, il suffira d'utiliser la commande ``deactivate``
Installation de Django et création du répertoire de travail
===========================================================
Comme l'environnement est activé, on peut à présent y installer Django. La librairie restera indépendante du reste du système, et ne polluera pas les autres projets.
C'est parti: ``pip install django``!
.. code-block:: shell
$ pip install django
Collecting django
Downloading Django-X.Y.Z
100% |################################|
Installing collected packages: django
Successfully installed django-X.Y.Z
Les commandes de création d'un nouveau site sont à présent disponibles, la principale étant ``django-admin startproject``. Par la suite, nous utiliserons ``manage.py``, qui constitue un *wrapper* autour de `django-admin`.
Pour démarrer notre projet, nous lançons donc ``django-admin startproject gwift``.
.. code-block:: shell
$ django-admin startproject gwift
Cette action a pour effet de créer un nouveau dossier ``gwift``, dans lequel on trouve la structure suivante:
.. code-block:: shell
$ tree gwift
gwift
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
C'est sans 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. Tant qu'on y est, nous pouvons rajouter les répertoires utiles à la gestion de notre projet, à savoir la documentation, les dépendances et le README:
.. code-block:: shell
$ mkdir docs requirements
$ touch docs/README.md
.. code-block:: shell
$ tree gwift
gwift
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
|-- docs/
|-- requirements/
|-- README
Chacun de ces fichiers sert à:
* ``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.
* ``wsgi.py`` contient la définition de l'interface `WSGI <https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface>`_, qui permettra à votre serveur Web (Nginx, Apache, ...) de faire un pont vers votre projet.
.. todo:: refaire un beau ``tree`` tout propre, à l'occasion.
.. todo:: déplacer la configuration dans un répertoire ``config`` à part.
Gestion des dépendances
=======================
Comme nous venons d'ajouter une dépendance à notre projet, nous allons 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 utilité:
* ``base.txt``
* ``dev.txt``
* ``staging.txt``
* ``production.txt``
Au début de chaque fichier, il suffira 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, ajoutez simplement ``django`` dans le fichier ``requirements/base.txt``.
.. code-block:: shell
$ echo django >> requirements/base.txt
Par la suite, il vous faudra **absolument** spécifier les versions à utiliser: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets. Des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc. Si vous voulez être sûr et certain que le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances. Avec les mécanismes d'intégration continue et de tests unitaires, on verra plus loin comment se prémunir d'un changement inattendu.
Structure finale de l'environnement
===================================
Nous avons donc la strucutre finale pour notre environnement de travail:
.. code-block:: shell
$ tree ~/gwift-project
gwift
├── docs
│   └── README.md
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
│   manage.py
└── requirements
└── base.txt

View File

@ -1,261 +0,0 @@
****************************************
Construire des applications maintenables
****************************************
Pour cette section, je me base d'un résumé de l'ebook **Building Maintenable Software** disponible chez `O'Reilly <http://shop.oreilly.com/product/0636920049555.do`_ qui vaut clairement le détour pour poser les bases d'un projet.
Ce livre répartit un ensemble de conseils parmi quatre niveaux de composants:
* Les méthodes et fonctions
* Les classes
* Les composants
* Et de manière plus générale.
Au niveau des méthodes et fonctions
===================================
* Gardez vos méthodes/fonctions courtes. Pas plus de 15 lignes, en comptant les commentaires. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9% de plus de 60 lignes; pas plus de 22.3% de plus de 30 lignes, au plus 43.7% de plus de 15 lignes et au moins 56.3% en dessous de 15 lignes). Oui, c'est dur à tenir, mais faisable.
* Conserver une complexité de McCabe en dessous de 5, c'est-à-dire avec quatre branches au maximum. A nouveau, si on a une méthode avec une complexité cyclomatique de 15, la séparer en 3 fonctions avec une complexité de 5 conservera globalement le nombre 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
* N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc., c'est juste mal: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées. C'est aussi une forme de régression.
* Conservez de petites interfaces. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe, plus facile à tester.
Au niveau des classes
=====================
* Privilégiez un couplage faible entre vos classes. Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences. L'implémentation du service ``UserNotificationsService`` ne doit pas forcément se trouver embarqué dans une classe ``UserService``. De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction. La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée.
Au niveau des composants
========================
* Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants également. Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'on ne puisse contacter trop facilement des couches séparées de l'architecture. Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "juste parce qu'elle en a la possibilité". Vous pourrez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que `IFileTransfer` avec ses méthodes `put` et `get`, et non pas les détails d'implémentation complet d'une classe `FtpFileTransfer` ou `SshFileTransfer`).
* Conserver un bon balancement au niveau des composants: évitez qu'un composant **A** ne soit un énorme mastodonte, alors que le composant juste à côté n'est capable que d'une action. De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités plus faciles à gérer. Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que ces composants soit approximativement de même taille.
De manière plus générale
========================
* Conserver une densité de code faible: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisant du refactoring (intensif?), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux. Mais surtout en évitant de copier/coller bêtement du code existant.
* Automatiser les tests, ajouter un environnement d'intégration continue dès le début du projet et vérifier par des outils les points ci-dessus.
***********
En pratique
***********
Par rapport aux points repris ci-dessus, l'environnement Python et le framework Django proposent un ensemble d'outils intégrés qui permettent de répondre à chaque point. Avant d'aller plus loin, donc, un petit point sur les conventions, les tests (unitaires, orientés comportement, basés sur la documentation, ...), la gestion de version du code et sur la documentation. Plus que dans tout langage compilé, ceux-ci sont pratiquement obligatoires. Vous pourrez les voir comme une perte de temps dans un premier temps, mais nous vous promettons qu'ils vous en feront gagner par la suite.
PEP8
====
Le langage Python fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le `Benevolent Dictator For Life <http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life>`_.
La PEP qui nous intéresse plus particulièrement pour la suite est la `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_, ou "Style Guide for Python Code". Elle spécifie des conventions d'organisation et de formatage de code Python, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, etc. En bref, elle décrit comment écrire du code proprement pour que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (``.``, le nom d'un répertoire, le nom d'un fichier ``.py``, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options ``--statistics -qq``.
.. code-block:: shell
$ pep8 . --statistics -qq
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez ``pyflakes``: il analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
Finalement, la solution qui couvre ces deux domaines existe et s'intitule `flake8 <https://github.com/PyCQA/flake8>`_. Sur base la même interface que ``pep8``, vous aurez en plus tous les avantages liés à ``pyflakes`` concernant votre code source.
PEP257
======
.. todo:: à remplir avec ``pydocstyle``.
Tests
=====
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe ``TestCase`` depuis le package ``django.test``:
.. code-block:: python
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
Couverture de code
==================
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet `coverage <https://pypi.python.org/pypi/coverage/>`_, qui se charge d'évaluer le pourcentage de code couvert par les tests. 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.
.. code-block:: shell
# requirements/base.text
[...]
coverage
django_coverage_plugin
.. code-block:: shell
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
.. todo:: le bloc ci-dessous est à revoir pour isoler la configuration.
.. code-block:: shell
$ 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
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. 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:
.. code-block:: shell
# 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."
Complexité de McCabe
====================
La `complexité cyclomatique <https://fr.wikipedia.org/wiki/Nombre_cyclomatique>`_ (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:
.. code-block:: python
if True == True:
pass # never happens
# continue ...
La condition existe, mais on ne passera jamais dedans. A l'inverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:
.. code-block:: python
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusqu'à arriver à l'impression à l'écran et à la valeur de retour. La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. Si on rencontre une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour s'assurer que la couverture est complète. On devrait donc trouver:
1. Un test pour entrer (ou non) dans la condition ``a == b``
2. Un test pour entrer (ou non) dans la condition ``b == c``
3. Un test pour entrer (ou non) dans la condition ``c == d``
4. Un test pour entrer (ou non) dans la condition ``d == e``
5. Et s'assurer que n'importe quel autre cas retournera la valeur ``None``.
On a donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
.. note::
Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale
A nouveau, un greffon pour ``flake8`` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre ``--max-complexity``. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
Documentation
=============
Il existe plusieurs manières de générer la documentation d'un projet. Les plus connues sont `Sphinx <http://sphinx-doc.org/>`_ et `MkDocs <http://www.mkdocs.org/>`_. Le premier a l'avantage d'être plus reconnu dans la communauté Python que l'autre, de pouvoir *parser* le code pour en extraire la documentation et de pouvoir lancer des `tests orientés documentation <https://duckduckgo.com/?q=documentation+driven+development&t=ffsb>`_. A contrario, votre syntaxe devra respecter `ReStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans son résultat.
Dans l'immédiat, nous nous contenterons d'avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/...). Dans la continuié de ``Flake8``, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.
.. note::
voir si il ne faudrait pas mieux passer par pydocstyle.
.. code-block:: shell
$ pip install flake8_docstrings
Lancez ensuite `flake8` avec la commande ``flake8 . --exclude="migrations"``. Sur notre projet (presque) vide, le résultat sera le suivant:
.. code-block:: shell
$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused
Bref, on le voit: nous n'avons que très peu de modules, et aucun d'eux n'est commenté.
En plus de cette méthode, Django permet également de rendre la documentation accessible depuis son interface d'administration.
PyLint
======
PyLint est la version **++**, pour ceux qui veulent un code propre et sans bavure.
.. todo:: à développer
Gestion de version du code
==========================
Il existe plusiseurs outils permettant de gérer les versions du code, dont les plus connus sont `git <https://git-scm.com/>`_ et `mercurial <https://www.mercurial-scm.org/>`_.
Dans notre cas, nous utilisons git et hebergons le code et le livre directement sur le gitlab de `framasoft <https://git.framasoft.org/>`_