Delete old asciidoc sources

This commit is contained in:
Fred Pauchet 2022-09-10 17:12:16 +02:00
parent 1a97286bb0
commit 43590682bc
48 changed files with 0 additions and 5044 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -1,156 +0,0 @@
*************
Environnement
*************
Préparation
===========
On prépare l'environement pour accueillir notre application Django. On considère que le serveur est un système GNU/Linux, basé sur une distribution Debian ou Ubuntu. Si vous vous basez sur un autre système d'exploitation ou une autre distribution, adaptez en fonction.
Il faut d'abord rajouter certains paquets qui seront nécessaires pour compiler certains module Python:
.. code-block:: shell
$$$ aptitude install libpq-dev python3-dev
On créé un utilisateur dédié, pour limiter les accès au serveur dans le cas où notre application serait piratée.
.. code-block:: shell
$$$ groupadd --system webapps
$$$ useradd --system --gid webapps --shell /bin/bash --home /webapps/gwift gwift
Ensuite, on crée le repertoire où se trouvera notre application et on lui attribue le bon utilisateur:
.. code-block:: shell
$$$ mkdir -p /webapps/gwift
$$$ chown gwift:webapps /webapps/gwift
Puis on crée notre environement virtuel:
.. code-block:: shell
$$$ su - gwift
gwift@gwift:~$ mkvirtualenv -p /usr/bin/python3 gwift
Already using interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in gwift/bin/python3
Also creating executable in gwift/bin/python
Installing setuptools, pip...done.
(gwift)gwift@gwift:~$
On peut maintenant cloner notre projet:
.. code-block:: shell
(gwift)gwift@gwift:~$ git clone git@framagit.org:Grimbox/gwift.git
Et installer les dépendances:
.. code-block:: shell
(gwift)gwift@gwift:~$ pip install -r requirements/production.txt
Le fichier ``production.txt`` contient les librairies pour gunicorn et PostgreSQL:
.. code-block:: shell
-r base.txt
gunicorn
psycopg2
setproctitle
Configuration
=============
Il ne nous reste plus qu'à mettre à jour la DB. On commance par créer le fichier de configuration de l'application en production:
.. code-block:: shell
(gwift)gwift@gwift:~$ touch gwift/gwift/settings/local.py
Et le contenu de local.py, avec la clé secrète, les paramètres pour se connecter à la DB et l'endroit où mettre les fichiers statics (voir point suivant):
.. code-block:: python
from .production import *
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'strong_secret_key'
# Allowed host needed to be defined in production
ALLOWED_HOSTS = ["sever_name.com", "www.sever_name.com"]
# Be sure to force https for csrf cookie
CSRF_COOKIE_SECURE = True
# Same for session cookie
SESSION_COOKIE_SECURE = True
# DB
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'gwift',
'USER': 'gwift_user',
'PASSWORD': 'gwift user password',
'HOST': 'localhost',
'PORT': '', # Set to empty string for default.
}
}
# Add static root
STATIC_ROOT = "/webapps/gwift/gwift/static"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
Finalement, on peut mettre à jour la DB et créer un super utilisateur:
.. code-block:: shell
(gwift)gwift@gwift:~$ python manage.py migrate
(gwift)gwift@gwift:~$ python manage.py createsuperuser
Fichiers statics
================
Django n'est pas fait pour servir les fichiers statics. Tous les fichiers statics doivent donc être déplacés dans un répertoire pour que Nginx puisse les servir facilement.
On commence par créer le répertoire où mettre les fichiers statics comme configuré dans le fichier local.py:
.. code-block:: shell
(gwift)gwift@gwift:~$ mkdir /webapps/gwift/gwift/static
Et on utilise django pour copier tous les fichiers statics au bon endroit:
.. code-block:: shell
(gwift)gwift@gwift:~$ python manage.py collectstatic
Test
====
On peut tester si tout fonctionne bien en lançant le serveur avec Django:
.. code-block:: shell
(gwift)gwift@gwift:~$ python manage.py runserver sever_name.com:8000
Et en se rendant sur server_name.com:8000/admin, on obtient:
.. image:: production/admin_without_static.png
:align: center
Comme on peut le voir, il n'y a pas de mise en forme de la page car les fichiers statics ne sont pas encore servis. Ils le seront par Nginx.

View File

@ -1,82 +0,0 @@
*************
Gunicorn
*************
Nous allons utiliser ``gunicorn`` comme serveur d'applications, le serveur fourni par django n'étant pas fait pour être utilisé en production.
Gunicorn a déjà été installé lors de la préparation de l'environnement. De même que ``setproctitle``, qui est nécessaire pour donner le nom de l'application aux processus python lancés par gunicorn.
Nous pouvons donc directement tester s'il fonctionne:
.. code-block:: shell
(gwift)gwift@gwift:~$ gunicorn gwift.wsgi:application --bind esever_name.com:8000
Et en se rendant sur server_name.com:8000/admin, on obtient la même chose qu'avec le serveur de django:
.. image:: production/admin_without_static.png
:align: center
Nous allons maintenant créer un fichier qui se chargera de lancer gunicorn correctement, que l'on sauve dans ``/webapps/gwift/gwift/bin/gunicorn_start``:
.. code-block:: shell
#!/bin/bash
# Define settings for gunicorn
NAME="gwift" # Name of the application
DJANGODIR=/webapps/gwift/gwift/gwift # Django project directory
SOCKFILE=/webapps/gwift/gwift/run/gunicorn.sock # we will communicte using this unix socket
USER=gwift # the user to run as
GROUP=webapps # the group to run as
NUM_WORKERS=3 # how many worker processes should Gunicorn spawn
DJANGO_SETTINGS_MODULE=gwift.settings # which settings file should Django use
DJANGO_WSGI_MODULE=gwift.wsgi # WSGI module name
echo "Starting $NAME as `whoami`"
# Activate the virtual environment
source /webapps/gwift/.virtualenvs/gwift/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
# Create the run directory if it doesn't exist
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR
# Start your Django Unicorn
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--bind=unix:$SOCKFILE \
--log-level=debug \
--log-file=-
Explications:
* NUM_WORKERS : gunicorn lance autant de worker que ce nombre. Un worker représente l'équivallant d'une instance de django et ne peut traiter qu'une requête à la fois. Traditionnellement, on créé autant de worker que le double du nombre de processeurs plus un.
* SOCKFILE : on configure gunicorn pour communiquer via un socket unix, ce qui est plus efficace de le faire par tcp/ip
Le script se charge donc de définir les options de configuration de gunicorn, de lancer l'environnement virtuel et ensuite gunicorn.
On peut le tester avec la commande suivante (hors environnement virtuel):
.. code-block:: shell
gwift@gwift:~$ source /webapps/gwift/gwift/bin/gunicorn_start
Et avec un petit ``ps`` dans un autre shell:
.. code-block:: shell
gwift@gwift:~$ ps -u gwift -F
UID PID PPID C SZ RSS PSR STIME TTY TIME CMD
gwift 31983 15685 0 18319 15084 1 Apr29 ? 00:00:01 gunicorn: master [gwift]
gwift 31992 31983 0 35636 29312 1 Apr29 ? 00:00:00 gunicorn: worker [gwift]
gwift 31993 31983 0 35634 29280 2 Apr29 ? 00:00:00 gunicorn: worker [gwift]
gwift 31994 31983 0 35618 29228 0 Apr29 ? 00:00:00 gunicorn: worker [gwift]
On voit donc bien qu'il y a un maître et trois workers.

View File

@ -1,158 +0,0 @@
*****
Nginx
*****
FrontEnd
========
Nginx est là pour agir en tant que front-end Web. A moins d'avoir configuré un mécanisme de cache type `Varnish <https://www.varnish-cache.org/>`_, c'est lui qui va recevoir la requête envoyée par l'utilisateur, gérer les fichiers et les informations statiques, et transmettre toute la partie dynamique vers Gunicorn.
Pour l'installer, on effectue la commande suivante:
.. code-block:: shell
$$$ aptitude install nginx
L'exemple ci-dessous se compose de plusieurs grandes parties: commune (par défaut), static, uploads, racine.
Partie commune
--------------
* Sur quel port Nginx doit-il écouter ? [80]
* client_max_body_size ?? [4G]
* Quel est le nom du serveur ? [ domain_name ]
* keepalive ??
* La compression Gzip doit-elle être activée ?
* Avec quels paramètres ? [gzip_comp_level 7, gzip_proxied any]
* Quels types de fichiers GZip doit-il prendre en compte ?
* Où les fichiers de logs doivent-ils être stockés ? [/logs/access.log & /logs/error.log]
Fichiers statiques
------------------
Pour les fichiers statiques, on définit un chemin ``/static`` dans le fichier de configuration, dans lequel on augmente le taux de compression et où on définit une durée de vie d'une semaine. En cas de non-présence du fichier, une erreur 404 est levée.
Uploads
-------
La partie ``uploads`` est très proche des autres fichiers statiques. Attention cependant que dans ce cas-ci, la configuration ne gérera pas l'authentification des utilisateurs pour l'accès à des ressources téléversées: si une personne possède le lien vers un fichier téléversé et qu'elle le transmet à quelqu'un d'autre, cette deuxième personne pourra y accéder sans aucun problème.
Si vous souhaitez implémenter un mécanisme d'accès géré, supprimez cette partie et implémenter la vôtre, directement dans l'application. Vous perdrez en performances, mais gagnerez en sécurité et en fonctionnalités.
Racine
------
La partie racine de votre domaine ou sous-domaine fera simplement le *pass_through* vers l'instance Gunicorn via un socket unix. En gros, et comme déjà expliqué, Gunicorn tourne en local et écoute un socket; la requête qui arrive sur le port 80 ou 443 est prise en compte par NGinx, puis transmise à Gunicorn sur le socket. Ceci est complétement transparent pour l'utilisateur de notre application.
On délare un upstream pour préciser à nginx comment envoyer les requêtes à gunicorn:
.. code-block:: shell
upstream gwift_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response (in case the Unicorn master nukes a
# single worker for timing out).
server unix:/directory/to/gunicorn.sock fail_timeout=0;
}
Au final
--------
.. code-block:: shell
upstream gwift_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response (in case the Unicorn master nukes a
# single worker for timing out).
server unix:/directory/to/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
client_max_body_size 4G;
server_name sever_name.com www.sever_name.com;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript text/x-js;
access_log {{ cwd }}/logs/access.log timed_combined;
error_log {{ cwd }}/logs/error.log;
location /static/ {
alias /webapps/gwift/gwift/static;
gzip on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript text/x-js;
gzip_comp_level 9;
expires 1w;
try_files $uri $uri/ =404;
}
location /uploads/ {
alias {{ uploads_folder }}/;
gzip on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript text/x-js;
gzip_comp_level 9;
expires 1w;
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_server;
}
}
Dans notre cas, et à adapter suivant les besoins, nous avons créé le fichier ``/etc/nginx/sites-available/gwift``, ainsi qu'un lien symbolique dans ``/etc/nginx/sites-enabled/gwift`` pour l'activer. Ensuite, nous pouvons redémarer nginx:
.. code-block:: shell
$$$ service nginx restart
Si on se connecte à notre server sur www.sever_name.com/admin, nous obtenons le site suivant:
.. image:: production/admin_with_static.png
:align: center
Où l'on peut voir que la mise en forme est correcte, ce qui signifie que les fichiers statics sont bien servis par nginx.
Modules complémentaires
=======================
PageSpeed
---------
Si le module `PageSpeed <https://github.com/pagespeed/ngx_pagespeed>`_ est installé, profitez-en pour ajouter la configuration suivante, à la fin de votre fichier de configuration:
.. code-block:: shell
pagespeed on;
pagespeed EnableFilters collapse_whitespace,insert_dns_prefetch,rewrite_images,combine_css,combine_javascript,flatten_css_imports,inline_css,rewrite_css,;
# Needs to exist and be writable by nginx.
pagespeed FileCachePath /var/nginx_pagespeed_cache;
# Ensure requests for pagespeed optimized resources go to the pagespeed handler
# and no extraneous headers get set.
location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" {
add_header "" "";
}
location ~ "^/ngx_pagespeed_static/" { }
location ~ "^/ngx_pagespeed_beacon$" { }
location /ngx_pagespeed_statistics { allow 127.0.0.1; deny all; }
location /ngx_pagespeed_global_statistics { allow 127.0.0.1; deny all; }
location /ngx_pagespeed_message { allow 127.0.0.1; deny all; }
L'intérêt est le suivant:
* Optimise les images (dégage les métadonnées, redimensionnement dynamique, compression)
* Minification des fichiers JavaScript
* Extension de la durée de vie du cache
* Légère réécriture des fichiers HTML
* `et plus encore <https://developers.google.com/speed/pagespeed/module/config_filters#level>`_.

View File

@ -1,95 +0,0 @@
== Mise à jour de l'application
Une application sans aucun bug et avec toutes les fonctionnalités présentes du premier coup, on n'a jamais vu (sauf peut-être Chuck Norris?).
Vous serez amenés (souvent?) à faire des mises à jour de votre application.
Les étapes à ne surtout pas oublier sont :
. La récupération des nouvelles sources
. La mise à jour du schéma de la base de données
. La récupération des (nouveaux) fichiers statiques
. Le redémarrage de gunicorn, puisque les processus précédents devraient encore être en train de tourner - si ce n'est pas le cas, vous aurez sûrement mis une page d'erreur avec une licorne en place ;-)
=== Récupération des sources
Si vous avez suivi ce guide jusqu'ici, vos sources devraient se trouver dans le répertoire `/webapps/gwift` d'un utilisateur. Suivant la sécurité mise en place, vous aurez deux possibilités:
. Soit les sources sont toujours liées au dépôt Git/Mercurial/Whatever
. Soit, vous devrez télécharger une archive contenant les fichiers.
Dans le premier cas :
[source,bash]
----
cd ~/webapps/gwift
git fetch
git checkout <version_number>
----
Dans le second cas :
[source,bash]
----
wget -O ...
unzip / tar xvfz / ...
chown gwift:webapps ...
----
Et dans les deux cas:
[source,bash]
----
source ~/.venvs/gwift/bin/activate
python manage.py migrate
python manage.py collectstatic
supervisorctl reload
----
NOTE: j'avais bidouillé un truc avec la documentation de Fabric, mais je pense que je ne l'avais jamais essayé :-)
[source]
----
*****************************
Automatisation du déploiement
*****************************
Pour automatiser le déploiement, il existe `Fabric <http://www.fabfile.org/>`_.
* **Problème**: cette librairie n'existe que pour Python2.7.
* **Avantage**: on peut écrire du code semblable à `ceci <https://github.com/UrLab/incubator/blob/master/fabfile.py>`_...
.. code-block:: python
from fabric.api import run, cd
from fabric.context_managers import prefix
def deploy():
code_dir = '/home/www-data/incubator'
with cd(code_dir), prefix('source ve/bin/activate'):
run('sudo supervisorctl stop incubator')
run("./save_db.sh")
run("git pull")
run("pip install -r requirements.txt --upgrade -q")
run("./manage.py collectstatic --noinput -v 0")
run("./manage.py makemigrations")
run("./manage.py migrate")
run('sudo supervisorctl start incubator')
En gros:
1. On se place dans le bon répertoire
2. On arrête le superviseur
3. On sauve les données de la base de données
4. On charge la dernière version depuis le dépôt Git
5. On met les dépendances à jour (en mode silencieux)
6. On agrège les fichiers statiques
7. On lance les migrations
8. Et on relance le superviseur.
Avec un peu de chances, l'instance est à jour.
----
IMPORTANT: y'a quand même un truc un peu foireux, c'est que l'utilisateur ci-dessus doit passer par root (ou sudo) pour redémarrer supervisorctl. C'est un peu moyen. Voir s'il n'y a pas un peu mieux comme méthode.

View File

@ -1,20 +0,0 @@
[glossary]
= Glossaire
http:: _HyperText Transfer Protocol_, ou plus généralement le protocole utilisé (et détourné) pour tout ce qui touche au **World Wide Web**. Il existe beaucoup d'autres protocoles d'échange de données, comme https://fr.wikipedia.org/wiki/Gopher[Gopher], https://fr.wikipedia.org/wiki/File_Transfer_Protocol[FTP] ou https://fr.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol[SMTP].
IaaS:: _Infrastructure as a Service_, où un tiers vous fournit des machines (généralement virtuelles) que vous devrez ensuite gérer en bon père de famille. L'IaaS propose souvent une API, qui vous permet d'intégrer la durée de vie de chaque machine dans vos flux - en créant, augmentant, détruisant une machine lorsque cela s'avère nécessaire.
MVC:: Le modèle _Model-View-Controler_ est un patron de conception autorisant un faible couplage entre la gestion des données (le _Modèle_), l'affichage et le traitement de celles (la _Vue_) et la glue entre ces deux composants (au travers du _Contrôleur_). https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller[Wikipédia]
ORM:: _Object Relational Mapper_, où une instance est directement (ou à proximité) liée à un mode de persistance de données.
PaaS:: _Platform as a Service_, qui consiste à proposer les composants d'une plateforme (Redis, PostgreSQL, ...) en libre service et disponibles à la demande (quoiqu'après avoir communiqué son numéro de carte de crédit...).
POO:: La _Programmation Orientée Objet_ est un paradigme de programmation informatique. Elle consiste en la définition et l'interaction de briques logicielles appelées objets ; un objet représente un concept, une idée ou toute entité du monde physique, comme une voiture, une personne ou encore une page d'un livre. Il possède une structure interne et un comportement, et il sait interagir avec ses pairs. Il s'agit donc de représenter ces objets et leurs relations ; l'interaction entre les objets via leurs relations permet de concevoir et réaliser les fonctionnalités attendues, de mieux résoudre le ou les problèmes. Dès lors, l'étape de modélisation revêt une importance majeure et nécessaire pour la POO. C'est elle qui permet de transcrire les éléments du réel sous forme virtuelle. https://fr.wikipedia.org/wiki/Programmation_orient%C3%A9e_objet[Wikipédia]
S3:: Amazon _Simple Storage Service_ consiste en un système d'hébergement de fichiers, quels qu'ils soient.
Il peut s'agir de fichiers de logs, de données applications, de fichiers média envoyés par vos utilisateurs, de vidéos et images ou de données de sauvegardes.
.https://aws.amazon.com/fr/s3/
image:images/amazon-s3-arch.png[]

View File

@ -1,155 +0,0 @@
= Minor swing with Django
Cédric Declerfayt <jaguarondi27@gmail.com>; Fred Pauchet <fred@grimbox.be>
:doctype: book
:toc:
:toclevels: 1
:sectnums:
:chapter-label: Chapitre
:bibtex-file: source/references.bib
:bibtex-order: alphabetical
:bibtex-throw: true
:source-highlighter: rouge
:icons: font
[preface]
== Licence
Ce travail est licencié sous Attribution-NonCommercial 4.0 International Attribution-NonCommercial 4.0 International
This license requires that reusers give credit to the creator.
It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, for noncommercial purposes only.
* *BY*: Credit must be given to you, the creator.
* *NC*: Only noncommercial use of your work is permitted. Noncommercial means not primarily intended for or directed towards commercial advantage or monetary compensation.
https://creativecommons.org/licenses/by-nc/4.0/?ref=chooser-v1
La seule exception concerne les morceaux de code (non attribués), disponibles sous licence https://mit-license.org/[MIT].
[preface]
== Préface
[quote,Robert C. Martin]
The only way to go fast, is to go well
Nous n'allons pas vous mentir: il existe enormément de tutoriaux très bien réalisés sur "_Comment réaliser une application Django_" et autres "_Déployer votre code en 2 minutes_".
Nous nous disions juste que ces tutoriaux restaient relativement haut-niveaux et se limitaient à un contexte donné, sans réellement préparer à la maintenance et au suivi de l'application nouvellement développée.
L'idée du texte ci-dessous est de jeter les bases d'un bon développement, en survolant l'ensemble des outils permettant de suivre des lignes directrices reconnues, de maintenir une bonne qualité de code au travers des différentes étapes menant jusqu'au déploiement et de s'assurer du maintient correct de la base de code, en permettant à n'importe qui de reprendre ce qui aura déjà été écrit.
Ces idées ne s'appliquent pas uniquement à Django et à son cadre de travail, ni même au langage Python.
Ces deux sujets sont cependant de bons candidats et leur cadre de travail est bien défini, documenté et suffisamment flexible.
Django se présente comme un _Framework Web pour perfectionnistes ayant des deadlines_ cite:[django] et suit ces quelques principes cite:[django_design_philosophies]:
* Faible couplage et forte cohésion, pour que chaque composant dispose de son indépendance, en n'ayant aucune connaissance des autres couches applicatives. Ainsi, le moteur de rendu ne connait absolument rien l'existence du moteur de base de données, tout comme le système de vues ne sait pas quel moteur de rendu est utilisé.
* Plus de fonctionnalités avec moins de code: chaque application Django doit utiliser le moins de code possible
* _Don't repeat yourself_, chaque concept ou morceau de code ne doit être présent qu'à un et un seul endroit de vos dépôts.
* Rapidité du développement, en masquant les aspects fastidieux du développement web actuel
Mis côte à côte, le suivi de ces principes permet une bonne stabilité du projet à moyen et long terme.
Comme nous le verrons par la suite, et sans être parfait, Django offre une énorme flexibilité qui permet de se laisser le maximum d'options ouvertes tout en permettant d'expérimenter facilement plusieurs pistes, jusqu'au moment de prendre une vraie décision.
Dans la majorité des cas problématiques pouvant être rencontrés lors du développement d'une application Web, Django proposera une solution pragmatique, compréhensible et facile à mettre en place.
En résumé de ce paragraphe, pour tout problème commun, vous disposerez d'une solution logique.
Tout pour plaire à n'importe quel directeur IT.
*Dans la première partie*, nous verrons comment partir d'un environnement sain, comment le configurer correctement, comment installer Django de manière isolée et comment démarrer un nouveau projet.
Nous verrons rapidement comment gérer les dépendances, les versions et comment appliquer et suivre un score de qualité de notre code.
Ces quelques points pourront être appliqués pour n'importe quel langage ou cadre de travail.
Nous verrons aussi que la configuration proposée par défaut par le framework n'est pas idéale dans la majorité des cas.
Pour cela, nous présenterons différents outils, la rédaction de tests unitaires et d'intégration pour limiter les régressions, les règles de nomenclature et de contrôle du contenu, comment partir d'un squelette plus complet, ainsi que les bonnes étapes à suivre pour arriver à un déploiement rapide et fonctionnel avec peu d'efforts.
A la fin de cette partie, vous disposerez d'un code propre et d'un projet fonctionnel, bien qu'encore un peu inutile.
*Dans la deuxième partie*, nous aborderons les grands principes de modélisation, en suivant les lignes de conduites du cadre de travail.
Nous aborderons les concepts clés qui permettent à une application de rester maintenable, les formulaires, leurs validations, comment gérer les données en entrée, les migrations de données et l'administration.
*Dans la troisième partie*, nous détaillerons précisément les étapes de déploiement, avec la description et la configuration de l'infrastructure, des exemples concrets de mise à disposition sur deux distributions principales (Debian et CentOS), sur une _*Plateform as a Service*_, ainsi que l'utilisation de Docker et Docker-Compose.
Nous aborderons également la supervision et la mise à jour d'une application existante, en respectant les bonnes pratiques d'administration système.
*Dans la quatrième partie*, nous aborderons les architectures typées _entreprise_, les services et les différentes manières de structurer notre application pour faciliter sa gestion et sa maintenance, tout en décrivant différents types de scénarii, en fonction des consommateurs de données.
*Dans la cinquième partie*, nous mettrons ces concepts en pratique en présentant le développement en pratique de deux applications, avec la description de problèmes rencontrés et la solution qui a été choisie: définition des tables, gestion des utilisateurs, ... et mise à disposition.
=== Pour qui ?
Avant tout, pour moi.
Comme le disait le Pr Richard Feynman: "_Si vous ne savez pas expliquer quelque chose simplement, c'est que vous ne l'avez pas compris_".
footnote:[Et comme l'ajoutait Aurélie Jean dans de L'autre côté de la machine: _"Si personne ne vous pose de questions suite à votre explication, c'est que vous n'avez pas été suffisamment clair !"_ cite:[other_side]]
Ce livre s'adresse autant au néophyte qui souhaite se lancer dans le développement Web qu'à l'artisan qui a besoin d'un aide-mémoire et qui ne se rappelle plus toujours du bon ordre des paramètres, ou à l'expert qui souhaiterait avoir un aperçu d'une autre technologie que son domaine privilégié de compétences.
Beaucoup de concepts présentés peuvent être oubliés ou restés inconnus jusqu'au moment où ils seront réellement nécessaires.
A ce moment-là, pour peu que votre mémoire ait déjà entraperçu le terme, il vous sera plus facile d'y revenir et de l'appliquer.
=== Pour aller plus loin
Il existe énormément de ressources, autant spécifiques à Django que plus généralistes.
Il ne sera pas possible de toutes les détailler; faites un tour sur
* https://duckduckgo.com,
* https://stackoverflow.com,
* https://ycombinator.com,
* https://lobste.rs/,
* https://lecourrierduhacker.com/
* ou https://www.djangoproject.com/.
Restez curieux, ne vous enclavez pas dans une technologie en particulier et gardez une bonne ouverture d'esprit.
=== Conventions
NOTE: Les notes indiquent des anecdotes.
TIP: Les conseils indiquent des éléments utiles, mais pas spécialement indispensables.
IMPORTANT: Les notes importantes indiquent des éléments à retenir.
CAUTION: Ces éléments indiquent des points d'attention. Les retenir vous fera gagner du temps en débuggage.
WARNING: Les avertissements indiquent un (potentiel) danger ou des éléments pouvant amener des conséquences pas spécialement sympathiques.
Les morceaux de code source seront présentés de la manière suivante:
[source,python,highlight=6]
----
# <folder>/<fichier>.<extension> <1>
def function(param):
""" <2>
"""
callback() <3>
----
<1> L'emplacement du fichier ou du morceau de code présenté, sous forme de commentaire
<2> Des commentaires, si cela s'avère nécessaire
<3> Les parties importantes ou récemment modifiées seront surlignées.
TIP: La plupart des commandes qui seront présentées dans ce livre le seront depuis un shell sous GNU/Linux.
Certaines d'entre elles pourraient devoir être adaptées si vous utilisez un autre système d'exploitation (macOS) ou n'importe quelle autre grosse bouse commerciale.
include::part-1-workspace/_main.adoc[]
include::part-3-data-model/_index.adoc[]
include::part-2-deployment/_main.adoc[]
include::part-4-services-oriented-applications/_main.adoc[]
include::part-5-go-live/_index.adoc[]
include::part-9-resources/_index.adoc[]
include::glossary.adoc[]
[index]
== Index
== Bibliographie
bibliography::[]

View File

@ -1,160 +0,0 @@
= Déploiement
[quote,Robert C. Martin, Clean Architecture, Chapitre 15, page 137]
To be effective, a software system must be deployable.
The higher the cost of deployements, the less useful the system is.
A goal of a software architecture, then, should be to make a system that can be easily deployed with a single action.
Unfortunately, deployment strategy is seldom considered during initial development.
This leads to architectures that may be make the system easy to develop, but leave it very difficult to deploy.
Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local,
on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un serveur.
Il est du coup probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles puissent être mises à disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé.
Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque.
Lobjectif est qu'il soit rapide et fiable.
Ceci peut être atteint au travers dun partitionnement correct, incluant le fait que le composant principal sassure que chaque sous-composant est correctement démarré intégré et supervisé.
Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes.
A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d'un tronc commun au développement cite:[devops_handbook].
Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux versions), puis redémarrer les services adéquats, et la procédure complète se résumera à quelques lignes d'un script bash.
[quote,DevOps Handbook, Introduction, page 8]
Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and disruptions such as service outages, service impairments, or security or compliance failures.
Le serveur que django met à notre disposition _via_ la commande `runserver` est extrêmement pratique, mais il est uniquement prévu pour la phase développement: en production, il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...).
De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l'on connait ses limites et que l'on se limite à un utilisateur à la fois.
En production, il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanés.
En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données.
En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou ou simplement parce que le serveur répondra trop lentement.
L'objectif de cette partie est de parcourir les différentes possibilités qui s'offrent à nous en termes de déploiement, tout en faisant en sorte que le code soit le moins couplé possible à sa destination de production.
L'objectif est donc de faire en sorte qu'une même application puisse être hébergées par plusieurs hôtes sans avoir à subir de modifications.
Nous vous renvoyons vers les 12-facteurs dont nous avons déjà parlé et qui vous énormément nous aider, puisque ce sont des variables d'environnement qui vont réellement piloter le câblage entre l'application, ses composants et son hébergeur.
RedHat proposait récemment un article intitulé _*What Is IaaS*_, qui présentait les principales différences entre types d'hébergement.
.L'infrastructure en tant que service, cc. _RedHat Cloud Computing_
[link=https://www.redhat.com/fr/topics/cloud-computing/what-is-iaas]
image::images/deployment/iaas_focus-paas-saas-diagram.png[]
Ainsi, on trouve:
1. Le déploiment _on-premises_ ou _on-site_
2. Les _Infrastructures as a service_ ou _((IaaS))_
3. Les _Platforms as a service_ ou _((PaaS))_
4. Les _Softwares as a service_ ou _((SaaS))_, ce dernier point nous concernant moins, puisque c'est nous qui développons le logiciel.
Dans cette partie, nous aborderons les points suivants:
1. Définir l'infrastructure et les composants nécessaires à notre application
2. Configurer l'hôte qui hébergera l'application et y déployer notre application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt. A ce stade, nous aurons déjà une application disponible.
3. Configurer les outils nécessaires à la bonne exécution de ce code et de ses fonctionnalités: les différentes méthodes de supervision de l'application, comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter correctement l'information.
== Infrastructure & composants
Pour une mise ne production, le standard _de facto_ est le suivant:
* Nginx comme reverse proxy
* HAProxy pour la distribution de charge
* Gunicorn ou Uvicorn comme serveur d'application
* Supervisor pour le monitoring
* PostgreSQL ou MySQL/MariaDB comme bases de données.
* Celery et RabbitMQ pour l'exécution de tâches asynchrones
* Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).
* Sentry, pour le suivi des bugs
Si nous schématisons l'infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante:
. L'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. Le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. Le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. La requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. Elle est ensuite transmise par socket et est prise en compte par un des _workers_ (= un processus Python) instancié par Gunicorn. Si l'un de ces travailleurs venait à planter, il serait automatiquement réinstancié par Supervisord.
. Qui la transmet ensuite à l'un de ses _workers_ (= un processus Python).
. Après exécution, une réponse est renvoyée à l'utilisateur.
image::images/diagrams/architecture.png[]
=== Reverse proxy
Le principe du *proxy inverse* est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système.
Il serait tout à fait possible de rendre notre application directement accessible depuis l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats footnote:[https://fr.wikipedia.org/wiki/Proxy_inverse]
=== Load balancer
=== Workers
=== Supervision des processus
=== Base de données
=== Tâches asynchrones
=== Mise en cache
== Code source
Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
. et renvoyer une réponse à l'utilisateur.
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, ...
== Outils de supervision et de mise à disposition
=== Logs
include::logging.adoc[]
=== Logging
. Sentry via sentry_sdk
. Nagios
. LibreNMS
. Zabbix
Il existe également https://munin-monitoring.org[Munin], https://www.elastic.co[Logstash, ElasticSearch et Kibana (ELK-Stack)] ou https://www.fluentd.org[Fluentd].
== Méthode de déploiement
Nous allons détailler ci-dessous trois méthodes de déploiement:
* Sur une machine hôte, en embarquant tous les composants sur un même serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de configurer un _load balancer_, de routeur plusieurs basées de données, mais ce sera le premier cas de figure.
* Dans des containers, avec Docker-Compose.
* Sur une *Plateforme en tant que Service* (ou plus simplement, *((PaaS))*), pour faire abstraction de toute la couche de configuration du serveur.
include::debian.adoc[]
include::heroku.adoc[]
include::docker.adoc[]
WARNING: le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt source.
On peut aussi passer par fabric, ansible, chef ou puppet.
== Autres outils
Voir aussi devpi, circus, uswgi, statsd.
See https://mattsegal.dev/nginx-django-reverse-proxy-config.html
== Ressources
* https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/
* https://docs.djangoproject.com/fr/3.0/howto/deployment/[Déploiement].
* Let's Encrypt !

View File

@ -1,412 +0,0 @@
== Déploiement sur Debian
La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits. Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur *root*, car la moindre faille pourrait avoir des conséquences catastrophiques.
Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'étape de configuration, qui consistera à:
1. Déployer les sources
2. Démarrer un serveur implémentant une interface WSGI (**Web Server Gateway Interface**), qui sera chargé de créer autant de [.line-through]#petits lutins# travailleurs que nous le désirerons.
3. Démarrer un superviseur, qui se chargera de veiller à la bonne santé de nos petits travailleurs, et en créer de nouveaux s'il le juge nécessaire
4. Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes d'un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l'un des travailleurs.
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, ... Il existe des dizaines d'hébergements typés VPS (**Virtual Private Server**). A vous de choisir celui qui vous convient footnote:[Personnellement, j'ai un petit faible pour Hetzner Cloud].
[source,bash]
----
apt update
groupadd --system webapps <1>
groupadd --system gunicorn_sockets <2>
useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift <3>
mkdir -p /home/gwift <4>
chown gwift:webapps /home/gwift <5>
----
<1> On ajoute un groupe intitulé `webapps`
<2> On crée un groupe pour les communications via sockets
<3> On crée notre utilisateur applicatif; ses applications seront placées dans le répertoire `/home/gwift`
<4> On crée le répertoire home/gwift
<5> On donne les droits sur le répertoire /home/gwift
=== Installation des dépendances systèmes
La version 3.6 de Python se trouve dans les dépôts officiels de CentOS.
Si vous souhaitez utiliser une version ultérieure, il suffit de l'installer en parallèle de la version officiellement supportée par votre distribution.
Pour CentOS, vous avez donc deux possibilités :
[source,bash]
----
yum install python36 -y
----
Ou passer par une installation alternative:
[source,bash]
----
sudo yum -y groupinstall "Development Tools"
sudo yum -y install openssl-devel bzip2-devel libffi-devel
wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz
cd Python-3.8*/
./configure --enable-optimizations
sudo make altinstall <1>
----
<1> *Attention !* Le paramètre `altinstall` est primordial. Sans lui, vous écraserez l'interpréteur initialement supporté par la distribution, et cela pourrait avoir des effets de bord non souhaités.
=== Installation de la base de données
On l'a déjà vu, Django se base sur un pattern type https://www.martinfowler.com/eaaCatalog/activeRecord.html[ActiveRecords] pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données connus:
* SQLite (en natif, mais Django 3.0 exige une version du moteur supérieure ou égale à la 3.8)
* MariaDB (en natif depuis Django 3.0),
* PostgreSQL au travers de psycopg2 (en natif aussi),
* Microsoft SQLServer grâce aux drivers [...à compléter]
* Oracle via https://oracle.github.io/python-cx_Oracle/[cx_Oracle].
CAUTION: Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n'est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause d'un Oracle 12.1).
Ci-dessous, quelques procédures d'installation pour mettre un serveur à disposition. Les deux plus simples seront MariaDB et PostgreSQL, qu'on couvrira ci-dessous. Oracle et Microsoft SQLServer se trouveront en annexes.
==== PostgreSQL
On commence par installer PostgreSQL.
Par exemple, dans le cas de debian, on exécute la commande suivante:
[source,bash]
----
$$$ aptitude install postgresql postgresql-contrib
----
Ensuite, on crée un utilisateur pour la DB:
[source,bash]
----
$$$ su - postgres
postgres@gwift:~$ createuser --interactive -P
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
postgres@gwift:~$
----
Finalement, on peut créer la DB:
[source,bash]
----
postgres@gwift:~$ createdb --owner gwift_user gwift
postgres@gwift:~$ exit
logout
$$$
----
NOTE: penser à inclure un bidule pour les backups.
==== MariaDB
Idem, installation, configuration, backup, tout ça.
A copier de grimboite, je suis sûr d'avoir des notes là-dessus.
==== Microsoft SQL Server
==== Oracle
=== Préparation de l'environnement utilisateur
[source,bash]
----
su - gwift
cp /etc/skel/.bashrc .
cp /etc/skel/.bash_profile .
ssh-keygen
mkdir bin
mkdir .venvs
mkdir webapps
python3.6 -m venv .venvs/gwift
source .venvs/gwift/bin/activate
cd /home/gwift/webapps
git clone ...
----
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes suivantes:
[source,bash]
----
# en tant qu'utilisateur 'gwift'
source .venvs/gwift/bin/activate
pip install -U pip
pip install -r requirements/base.txt
pip install gunicorn
cd webapps/gwift
gunicorn config.wsgi:application --bind localhost:3000 --settings=config.settings_production
----
=== Configuration de l'application
[source,bash]
----
SECRET_KEY=<set your secret key here> <1>
ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/gwift/static
DATABASE= <2>
----
<1> La variable `SECRET_KEY` est notamment utilisée pour le chiffrement des sessions.
<2> On fait confiance à django_environ pour traduire la chaîne de connexion à la base de données.
=== Création des répertoires de logs
[source,text]
----
mkdir -p /var/www/gwift/static
----
=== Création du répertoire pour le socket
Dans le fichier `/etc/tmpfiles.d/gwift.conf`:
[source,text]
----
D /var/run/webapps 0775 gwift gunicorn_sockets -
----
Suivi de la création par systemd :
[source,text]
----
systemd-tmpfiles --create
----
=== Gunicorn
[source,bash]
----
#!/bin/bash
# defines settings for gunicorn
NAME="gwift"
DJANGODIR=/home/gwift/webapps/gwift
SOCKFILE=/var/run/webapps/gunicorn_gwift.sock
USER=gwift
GROUP=gunicorn_sockets
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production
DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`"
source /home/gwift/.venvs/gwift/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user $USER \
--bind=unix:$SOCKFILE \
--log-level=debug \
--log-file=-
----
=== Supervision, keep-alive et autoreload
Pour la supervision, on passe par Supervisor. Il existe d'autres superviseurs,
[source,bash]
----
yum install supervisor -y
----
On crée ensuite le fichier `/etc/supervisord.d/gwift.ini`:
[source,bash]
----
[program:gwift]
command=/home/gwift/bin/start_gunicorn.sh
user=gwift
stdout_logfile=/var/log/gwift/gwift.log
autostart=true
autorestart=unexpected
redirect_stdout=true
redirect_stderr=true
----
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/gwift
chown gwift:nagios /var/log/gwift
systemctl enable supervisord
systemctl start supervisord.service
systemctl status supervisord.service
● supervisord.service - Process Monitoring and Control Daemon
Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago
Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code=exited, status=0/SUCCESS)
Main PID: 2310 (supervisord)
CGroup: /system.slice/supervisord.service
├─2310 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf
├─2313 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2317 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2318 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2321 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2322 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
└─2323 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
ls /var/run/webapps/
----
On peut aussi vérifier que l'application est en train de tourner, à l'aide de la commande `supervisorctl`:
[source,bash]
----
$$$ supervisorctl status gwift
gwift RUNNING pid 31983, uptime 0:01:00
----
Et pour gérer le démarrage ou l'arrêt, on peut passer par les commandes suivantes:
[source,bash]
----
$$$ supervisorctl stop gwift
gwift: stopped
root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift
gwift: started
root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift
gwift: stopped
gwift: started
----
=== Configuration du firewall et ouverture des ports
et 443 (HTTPS).
[source,text]
----
firewall-cmd --permanent --zone=public --add-service=http <1>
firewall-cmd --permanent --zone=public --add-service=https <2>
firewall-cmd --reload
----
<1> On ouvre le port 80, uniquement pour autoriser une connexion HTTP, mais qui sera immédiatement redirigée vers HTTPS
<2> Et le port 443 (forcément).
=== Installation d'Nginx
[source]
----
yum install nginx -y
usermod -a -G gunicorn_sockets nginx
----
On configure ensuite le fichier `/etc/nginx/conf.d/gwift.conf`:
----
upstream gwift_app {
server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0;
}
server {
listen 80;
server_name <server_name>;
root /var/www/gwift;
error_log /var/log/nginx/gwift_error.log;
access_log /var/log/nginx/gwift_access.log;
client_max_body_size 4G;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /static/ { <1>
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; <2>
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_app;
}
}
----
<1> Ce répertoire sera complété par la commande `collectstatic` que l'on verra plus tard. L'objectif est que les fichiers ne demandant aucune intelligence soit directement servis par Nginx. Cela évite d'avoir un processus Python (relativement lent) qui doive être instancié pour servir un simple fichier statique.
<2> Afin d'éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1
=== Mise à jour
Script de mise à jour.
[source,bash]
----
su - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
kill -HUP `ps -C gunicorn fch -o pid | head -n 1` <1>
----
<1> https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file
=== Configuration des sauvegardes
Les sauvegardes ont été configurées avec borg: `yum install borgbackup`.
C'est l'utilisateur gwift qui s'en occupe.
----
mkdir -p /home/gwift/borg-backups/
cd /home/gwift/borg-backups/
borg init gwift.borg -e=none
borg create gwift.borg::{now} ~/bin ~/webapps
----
Et dans le fichier crontab :
----
0 23 * * * /home/gwift/bin/backup.sh
----
=== Rotation des jounaux
[source,bash]
----
/var/log/gwift/* {
weekly
rotate 3
size 10M
compress
delaycompress
}
----
Puis on démarre logrotate avec # logrotate -d /etc/logrotate.d/gwift pour vérifier que cela fonctionne correctement.
=== Ansible
TODO

View File

@ -1,22 +0,0 @@
=== Docker-Compose
(c/c Ced' - 2020-01-24)
Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter pour django.
D'abords, après avoir installer docker-compose et les dépendances sous debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root pour utiliser docker.
Ensuite, j'ai relancé mon pc car juste relancé un shell n'a pas suffit pour que je puisse utiliser docker avec mon compte.
Bon après c'est facile, un petit virtualenv pour cookiecutter, suivit d'une installation du template django.
Et puis j'ai suivi sans t https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html
Alors, il télécharge les images, fait un petit update, installe les dépendances de dev, install les requirement pip ...
Du coup, ça prend vite de la place:
image.png
L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque 1,5 GB d'un coup.
Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien faire ou presque.
La partie ci-dessous a été reprise telle quelle de https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html[la documentation de cookie-cutter-django].

View File

@ -1,240 +0,0 @@
== Déploiement sur Heroku
https://www.heroku.com[Heroku] est une _Plateform As A Service_ (((paas))), où vous choisissez le _service_ dont vous avez besoin (une base de données, un service de cache, un service applicatif, ...), vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l'hôte.
Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d'autant plus qu'il ne sera pas possible de modifier un fichier une fois qu'elle aura démarré: si vous souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et envoyer de nouveaux paramètres et recommencer le déploiement depuis le début.
.Invest in apps, not ops. Heroku handles the hard stuff — patching and upgrading, 24/7 ops and security, build systems, failovers, and more — so your developers can stay focused on building great apps.
image::images/deployment/heroku.png[]
Pour un projet de type "hobby" et pour l'exemple de déploiement ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un _Most Valuable Product_ en place.
La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un _bucket compatible S3_, par exemple chez Amazon, Scaleway ou OVH.
Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé.
Il suffit donc d'envoyer les sources de votre application vers ce dépôt pour qu'Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition.
Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l'équipe.
Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de _pipeline_, lorsque tous les tests unitaires et d'intégration auront été réalisés.
Au travers de la commande `heroku create`, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier `.git/config` ci-dessous:
[source,bash,highlight=13-15]
----
$ heroku create
Creating app... done, ⬢ young-temple-86098
https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-temple-86098.git
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "heroku"]
url = https://git.heroku.com/still-thicket-66406.git
fetch = +refs/heads/*:refs/remotes/heroku/*
----
IMPORTANT:
----
Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration.
Celle-ci se limite aux deux fichiers suivants:
* Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI
* Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`)
----
Après ce paramétrage, il suffit de pousser les changements vers ce nouveau dépôt grâce à la commande `git push heroku master`.
WARNING: Heroku propose des espaces de déploiements, mais pas d'espace de stockage.
Il est possible d'y envoyer des fichiers utilisateurs (typiquement, des media personnalisés), mais ceux-ci seront perdus lors du redémarrage du container.
Il est donc primordial de configurer correctement l'hébergement des fichiers média, de préférences sur un stockage compatible S3. (((s3)))
Prêt à vous lancer ? Commencez par créer un compte: https://signup.heroku.com/python.
=== Configuration du compte Heroku
+ Récupération des valeurs d'environnement pour les réutiliser ci-dessous.
Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre première application; heureusement, la documentation est super bien faite:
.Heroku: Commencer à travailler avec un langage
image::images/deployment/heroku-new-app.png[]
Installez ensuite la CLI (_Command Line Interface_) en suivant https://devcenter.heroku.com/articles/heroku-cli[la documentation suivante].
Au besoin, cette CLI existe pour:
. macOS, _via_ `brew `
. Windows, grâce à un https://cli-assets.heroku.com/heroku-x64.exe[binaire x64] (la version 32 bits existe aussi, mais il est peu probable que vous en ayez besoin)
. GNU/Linux, via un script Shell `curl https://cli-assets.heroku.com/install.sh | sh` ou sur https://snapcraft.io/heroku[SnapCraft].
Une fois installée, connectez-vous:
[source,bash]
----
$ heroku login
----
Et créer votre application:
[source,bash]
----
$ heroku create
Creating app... done, ⬢ young-temple-86098
https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-temple-86098.git
----
.Notre application est à présent configurée!
image::images/deployment/heroku-app-created.png[]
Ajoutons lui une base de données, que nous sauvegarderons à intervalle régulier:
[source,bash]
----
$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on ⬢ still-thicket-66406... free
Database has been created and is available
! This database is empty. If upgrading, you can transfer
! data from another database with pg:copy
Created postgresql-clear-39693 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
$ heroku pg:backups schedule --at '14:00 Europe/Brussels' DATABASE_URL
Scheduling automatic daily backups of postgresql-clear-39693 at 14:00 Europe/Brussels... done
----
TODO: voir comment récupérer le backup de la db :-p
[source,bash]
----
# Copié/collé de https://cookiecutter-django.readthedocs.io/en/latest/deployment-on-heroku.html
heroku create --buildpack https://github.com/heroku/heroku-buildpack-python
heroku addons:create heroku-redis:hobby-dev
heroku addons:create mailgun:starter
heroku config:set PYTHONHASHSEED=random
heroku config:set WEB_CONCURRENCY=4
heroku config:set DJANGO_DEBUG=False
heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production
heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)"
# Generating a 32 character-long random string without any of the visually similar characters "IOl01":
heroku config:set DJANGO_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A-HJ-NP-Za-km-z2-9' | head -c 32)/"
# Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com'
heroku config:set DJANGO_ALLOWED_HOSTS=
# Assign with AWS_ACCESS_KEY_ID
heroku config:set DJANGO_AWS_ACCESS_KEY_ID=
# Assign with AWS_SECRET_ACCESS_KEY
heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY=
# Assign with AWS_STORAGE_BUCKET_NAME
heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME=
git push heroku master
heroku run python manage.py createsuperuser
heroku run python manage.py check --deploy
heroku open
----
=== Configuration
Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement.
Pour un projet Django, cela comprend, à placer à la racine de votre projet:
1. Un fichier `requirements.txt` (qui peut éventuellement faire appel à un autre fichier, *via* l'argument `-r`)
2. Un fichier `Procfile` ([sans extension](https://devcenter.heroku.com/articles/procfile)!), qui expliquera la commande pour le protocole WSGI.
Dans notre exemple:
[source]
----
# requirements.txt
django==3.2.8
gunicorn
boto3
django-storages
----
[source,bash]
----
# Procfile
release: python3 manage.py migrate
web: gunicorn gwift.wsgi
----
=== Hébergement S3
Pour cette partie, nous allons nous baser sur l'https://www.scaleway.com/en/object-storage/[Object Storage de Scaleway].
Ils offrent 75GB de stockage et de transfert par mois, ce qui va nous laisser suffisament d'espace pour jouer un peu 😉.
image:images/deployment/scaleway-object-storage-bucket.png[]
L'idée est qu'au moment de la construction des fichiers statiques, Django aille simplement les héberger sur un espace de stockage compatible S3.
La complexité va être de configurer correctement les différents points de terminaison.
Pour héberger nos fichiers sur notre *bucket* S3, il va falloir suivre et appliquer quelques étapes dans l'ordre:
1. Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y en a - *littéralement* - des dizaines.
2. Ajouter la librairie `boto3`, qui s'occupera de "parler" avec ce type de protocole
3. Ajouter la librairie `django-storage`, qui va elle s'occuper de faire le câblage entre le fournisseur (*via* `boto3`) et Django, qui s'attend à ce qu'on lui donne un moteur de gestion *via* la clé [`DJANGO_STATICFILES_STORAGE`](https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-STATICFILES_STORAGE).
La première étape consiste à se rendre dans [la console Scaleway](https://console.scaleway.com/project/credentials), pour gérer ses identifiants et créer un jeton.
image:images/deployment/scaleway-api-key.png[]
Selon la documentation de https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings[django-storages], de https://boto3.amazonaws.com/v1/documentation/api/latest/index.html[boto3] et de https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/[Scaleway], vous aurez besoin des clés suivantes au niveau du fichier `settings.py`:
[source,python]
----
AWS_ACCESS_KEY_ID = os.getenv('ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME')
AWS_DEFAULT_ACL = 'public-read'
AWS_LOCATION = 'static'
AWS_S3_SIGNATURE_VERSION = 's3v4'
AWS_S3_HOST = 's3.%s.scw.cloud' % (AWS_S3_REGION_NAME,)
AWS_S3_ENDPOINT_URL = 'https://%s' % (AWS_S3_HOST, )
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3ManifestStaticStorage'
STATIC_URL = '%s/%s/' % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)
# General optimization for faster delivery
AWS_IS_GZIPPED = True
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
----
Configurez-les dans la console d'administration d'Heroku:
image:images/deployment/heroku-vars-reveal.png[]
Lors de la publication, vous devriez à présent avoir la sortie suivante, qui sera confirmée par le *bucket*:
[source,bash]
----
remote: -----> $ python manage.py collectstatic --noinput
remote: 128 static files copied, 156 post-processed.
----
image:images/deployment/gwift-cloud-s3.png[]
Sources complémentaires:
* [How to store Django static and media files on S3 in production](https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/)
* [Using Django and Boto3](https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/)

View File

@ -1,69 +0,0 @@
== Logging
La structure des niveaux de journaux est essentielle.
[quote,Dan North, former ToughtWorks consultant]
When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m. Low printer toner is not an ERROR.
* *DEBUG*: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application. Généralement, ce niveau est désactivé pour une application qui passe en production, sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement.
* *INFO*: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, ...
* *WARN*: Regroupe les informations qui pourraient potentiellement devenir des erreurs.
* *ERROR*: Indique les informations internes - Erreur lors de l'appel d'une API, erreur interne, ...
* *FATAL* (ou *EXCEPTION*): ... généralement suivie d'une terminaison du programme ;-) - Bind raté d'un socket, etc.
La configuration des _loggers_ est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore.
Il est ainsi possible de définir des formattages, gestionnaires (_handlers_) et loggers distincts, en fonction de nos applications.
Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements.
Il n'est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les `StreamHandler`.
La configuration que nous allons utiliser est celle-ci:
. Formattage: à définir - mais la variante suivante est complète, lisible et pratique: `{levelname} {asctime} {module} {process:d} {thread:d} {message}`
. Handler: juste un, qui définit un `StreamHandler`
. Logger: pour celui-ci, nous avons besoin d'un niveau (`level`) et de savoir s'il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de `propagate` à `True`.
[source,python]
----
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
},
'simple': {
'format': '{levelname} {asctime} {module} {message}',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': "verbose"
}
},
'loggers': {
'khana': {
'handlers': ['console'],
'level': env("LOG_LEVEL", default="DEBUG"),
'propagate': True,
},
}
}
----
Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant:
[source,python]
----
import logging
logger = logging.getLogger(__name__)
logger.debug('helloworld')
----
https://docs.djangoproject.com/en/stable/topics/logging/#examples[Par exemples].
Nous verrons plus loin (cf. ) comment configurer gunicorn pour intercepter correctement toutes ces informations.

View File

@ -1,41 +0,0 @@
= Principes fondamentaux
Dans ce chapitre, nous allons parler de plusieurs concepts fondamentaux au développement rapide d'une application utilisant Django.
Nous parlerons de modélisation, de métamodèle, de migrations, d'administration auto-générée, de traductions et de cycle de vie des données.
Django est un framework Web qui propose une très bonne intégration des composants et une flexibilité bien pensée: chacun des composants permet de définir son contenu de manière poussée, en respectant des contraintes logiques et faciles à retenir, et en gérant ses dépendances de manière autonome.
Pour un néophyte, la courbe d'apprentissage sera relativement ardue: à côté de concepts clés de Django, il conviendra également d'assimiler correctement les structures de données du langage Python, le cycle de vie des requêtes HTTP et le B.A-BA des principes de sécurité.
En restant dans les sentiers battus, votre projet suivra un patron de conception dérivé du modèle `MVC` (Modèle-Vue-Controleur), où la variante concerne les termes utilisés: Django les nomme respectivement Modèle-Template-Vue et leur contexte d'utilisation.
Dans un *pattern* MVC classique, la traduction immédiate du **contrôleur** est une **vue**.
Et comme nous le verrons par la suite, la **vue** est en fait le **template**.
La principale différence avec un modèle MVC concerne le fait que la vue ne s'occupe pas du routage des URLs; ce point est réalisé par un autre composant, interne au framework, graĉe aux différentes routes définies dans les fichiers `urls.py`.
* Le **modèle** (`models.py`) fait le lien avec la base de données et permet de définir les champs et leur type à associer à une table. _Grosso modo_*, une table SQL correspondra à une classe d'un modèle Django.
* La **vue** (`views.py`), qui joue le rôle de contrôleur: _a priori_, tous les traitements, la récupération des données, etc. doit passer par ce composant et ne doit (pratiquement) pas être généré à la volée, directement à l'affichage d'une page. En d'autres mots, la vue sert de pont entre les données gérées par la base et l'interface utilisateur.
* Le **template**, qui s'occupe de la mise en forme: c'est le composant qui s'occupe de transformer les données en un affichage compréhensible (avec l'aide du navigateur) pour l'utilisateur.
Pour reprendre une partie du schéma précédent, lorsqu'une requête est émise par un utilisateur, la première étape va consister à trouver une _route_ qui correspond à cette requête, c'est à dire à trouver la correspondance entre l'URL qui est demandée par l'utilisateur et la fonction du langage qui sera exécutée pour fournir le résultat attendu.
Cette fonction correspond au *contrôleur* et s'occupera de construire le *modèle* correspondant.
include::models.adoc[]
include::querysets.adoc[]
include::migrations.adoc[]
include::shell.adoc[]
include::admin.adoc[]
include::forms.adoc[]
include::auth.adoc[]
include::settings.adoc[]
include::context_processors.adoc[]
include::tests.adoc[]
== Conclusions

View File

@ -1,277 +0,0 @@
== 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.
Mais c'est faux.
L'administration est une sorte de tour de contrôle évoluée, un _back office_ sans transpirer; elle se base sur le modèle de données programmé et construit dynamiquement les formulaires qui lui est associé.
Elle joue avec les clés primaires, étrangères, les champs et types de champs par https://fr.wikipedia.org/wiki/Introspection[introspection], et présente tout ce qu'il faut pour avoir du https://fr.wikipedia.org/wiki/CRUD[CRUD], c'est-à-dire tout ce qu'il faut pour ajouter, lister, modifier ou supprimer des informations.
Son problème est qu'elle présente une courbe d'apprentissage asymptotique.
Il est *très* facile d'arriver rapidement à un bon résultat, au travers d'un périmètre de configuration relativement restreint.
Mais quoi que vous fassiez, il y a un moment où la courbe de paramétrage sera tellement ardue que vous aurez plus facile à développer ce que vous souhaitez ajouter en utilisant les autres concepts de Django.
Cette fonctionnalité doit rester dans les mains d'administrateurs ou de gestionnaires, et dans leurs mains à eux uniquement: il n'est pas question de donner des droits aux utilisateurs finaux (même si c'est extrêment tentant durant les premiers tours de roues).
Indépendamment de la manière dont vous allez l'utiliser et la configurer, vous finirez par devoir développer une "vraie" application, destinée aux utilisateurs classiques, et répondant à leurs besoins uniquement.
Une bonne idée consiste à développer l'administration dans un premier temps, en *gardant en tête qu'il sera nécessaire de développer des concepts spécifiques*.
Dans cet objectif, l'administration est un outil exceptionel, qui permet de valider un modèle, de créer des objets rapidement et de valider les liens qui existent entre eux.
C'est aussi un excellent outil de prototypage et de preuve de concept.
Elle se base sur plusieurs couches que l'on a déjà (ou on va bientôt) aborder (suivant le sens de lecture que vous préférez):
. Le modèle de données
. Les validateurs
. Les formulaires
. Les widgets
=== Le modèle de données
Comme expliqué ci-dessus, le modèle de données est constité d'un ensemble de champs typés et de relations.
L'administration permet de décrire les données qui peuvent être modifiées, en y associant un ensemble (basique) de permissions.
Si vous vous rappelez de l'application que nous avions créée dans la première partie, les URLs reprenaient déjà la partie suivante:
[source,python]
----
from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls), <1>
[...]
]
----
<1> Cette URL signifie que la partie `admin` est déjà active et accessible à l'URL `<mon_site>/admin`
C'est le seul prérequis pour cette partie.
Chaque application nouvellement créée contient par défaut un fichier `admin.py`, dans lequel il est possible de déclarer quel ensemble de données sera accessible/éditable.
Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits:
[source,python]
----
# gwift/wish/models.py
from django.db import models
class WishList(models.Model):
name = models.CharField(max_length=255)
class Item(models.Model):
name = models.CharField(max_length=255)
wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE)
----
Nous pouvons facilement arriver au résultat suivant, en ajoutant quelques lignes de configuration dans ce fichier `admin.py`:
[source,python]
----
from django.contrib import admin
from .models import Item, WishList <1>
admin.site.register(Item) <2>
admin.site.register(WishList)
----
<1> Nous importons les modèles que nous souhaitons gérer dans l'admin
<2> Et nous les déclarons comme gérables. Cette dernière ligne implique aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui n'activera simplement aucune opération de lecture ou modification.
Il nous reste une seule étape à réaliser: créer un nouvel utilisateur.
Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d'un _super-utilisateur_, que nous pouvons créer grâce à la commande `python manage.py createsuperuser`.
[source,bash]
----
λ python manage.py createsuperuser
Username (leave blank to use 'fred'): fred
Email address: fred@root.org
Password: ******
Password (again): ******
Superuser created successfully.
----
.Connexion au site d'administration
image::images/django/django-site-admin.png[align=center]
.Administration
image::images/django/django-site-admin-after-connection.png[align=center]
=== Quelques conseils de base
. 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 pour chaque instance de votre classe. Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l'on manipule. En plus, cette méthode est également appelée lorsque l'administration historisera une action (et comme cette étape sera inaltérable, autant qu'elle soit fixée dans le début).
. 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.
=== admin.ModelAdmin
La classe `admin.ModelAdmin` que l'on retrouvera principalement dans le fichier `admin.py` de chaque application contiendra la définition de ce que l'on souhaite faire avec nos données dans l'administration. Cette classe (et sa partie Meta)
=== L'affichage
Comme l'interface d'administration fonctionne (en trèèèès) gros comme un CRUD auto-généré, on trouve par défaut la possibilité de :
. Créer de nouveaux éléments
. Lister les éléments existants
. Modifier des éléments existants
. Supprimer un élément en particulier.
Les affichages sont donc de deux types: en liste et par élément.
Pour les affichages en liste, le plus simple consiste à jouer sur la propriété `list_display`.
Par défaut, la première colonne va accueillir le lien vers le formulaire d'édition.
On peut donc modifier ceci, voire créer de nouveaux liens vers d'autres éléments en construisant des URLs dynamiquement.
(Insérer ici l'exemple de Medplan pour les liens vers les postgradués :-))
Voir aussi comment personnaliser le fil d'Ariane ?
=== Les filtres
. list_filter
. filter_horizontal
. filter_vertical
. date_hierarchy
=== Les permissions
On l'a dit plus haut, il vaut mieux éviter de proposer un accès à l'administration à vos utilisateurs.
Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression.
Cela se joue au niveau du `ModelAdmin`, en implémentant les méthodes suivantes:
[source,python]
----
def has_add_permission(self, request):
return True
def has_delete_permission(self, request):
return True
def has_change_permission(self, request):
return True
----
On peut accéder aux informations de l'utilisateur actuellement connecté au travers de l'objet `request.user`.
.. NOTE: ajouter un ou deux screenshots :-)
=== Les relations
==== Les relations 1-n
Les relations 1-n sont implémentées au travers de formsets (que l'on a normalement déjà décrits plus haut). L'administration permet de les définir d'une manière extrêmement simple, grâce à quelques propriétés.
L'implémentation consiste tout d'abord à définir le comportement du type d'objet référencé (la relation -N), puis à inclure cette définition au niveau du type d'objet référençant (la relation 1-).
[source,python]
----
class WishInline(TabularInline):
model = Wish
class Wishlist(admin.ModelAdmin):
...
inlines = [WishInline]
...
----
Et voilà : l'administration d'une liste de souhaits (_Wishlist_) pourra directement gérer des relations multiples vers des souhaits.
==== Les auto-suggestions et auto-complétions
Parler de l'intégration de select2.
=== La présentation
Parler ici des `fieldsets` et montrer comment on peut regrouper des champs dans des groupes, ajouter un peu de javascript, ...
=== Les actions sur des sélections
Les actions permettent de partir d'une liste d'éléments, et autorisent un utilisateur à appliquer une action sur une sélection d'éléments. Par défaut, il existe déjà une action de *suppression*.
Les paramètres d'entrée sont :
. L'instance de classe
. La requête entrante
. Le queryset correspondant à la sélection.
[source,python]
----
def double_quantity(self, request, queryset):
for obj in queryset.all():
obj.field += 1
obj.save()
double_quantity.short_description = "Doubler la quantité des souhaits."
----
Et pour informer l'utilisateur de ce qui a été réalisé, on peut aussi lui passer un petit message:
[source,python]
----
if rows_updated = 0:
self.message_user(request, "Aucun élément n'a été impacté.")
else:
self.message_user(request, "{} élément(s) mis à jour".format(rows_updated))
----
=== La documentation
Nous l'avons dit plus haut, l'administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données.
Pour cela, il suffit de suivre les bonnes pratiques, puis https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/[d'activer la documentation à partir des URLs]:
[source,python]
----
----

View File

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

View File

@ -1,44 +0,0 @@
== _Context Processors_
Mise en pratique: un _context processor_ sert _grosso-modo_ à peupler l'ensemble des données transmises des vues aux templates avec des données communes.
Un context processor est un peu l'équivalent d'un middleware, mais entre les données et les templates, là où le middleware va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
[source,python]
----
# core/context_processors.py
import subprocess
def git_describe(request) -> str:
return {
"git_describe": subprocess.check_output(
["git", "describe", "--always"]
).strip(),
"git_date": subprocess.check_output(
["git", "show", "-s", r"--format=%cd", r"--date=format:%d-%m-%Y"]
),
}
----
Ceci aura pour effet d'ajouter les deux variables `git_describe` et `git_date` dans tous les contextes de tous les templates de l'application.
[source,python,highlight=12]
----
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates"),],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"core.context_processors.git_describe"
],
},
},
]
----

View File

@ -1,120 +0,0 @@
== Forms
[quote]
Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et que l'ensemble reste cohérent.
Il (le form) n'a pas à savoir que tu as implémenté des closure tables dans un graph dirigé acyclique.
Ou comment valider proprement des données entrantes.
image::images/xkcd-327.png[]
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. On peut proposer trois versions d'un même code, de la version simple (lecture du fichier csv et jonglage avec les indices de colonnes), puis une version plus sophistiquée (et plus lisible, à base de https://docs.python.org/3/library/csv.html#csv.DictReader[DictReader]), et la version +++ à base de form.
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 deux rôles importants:
. Valider des données, en plus de celles déjà définies au niveau du modèle
. Contrôler le rendu à appliquer aux champs.
Ils agissent come une glue entre l'utilisateur et la modélisation de vos structures de données.
=== Flux de validation
| .Validation
| .is_valid
| .clean_fields
↓ .clean_fields_machin
NOTE: A compléter ;-)
=== 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 datetime import date
from django import forms
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 et en passant par `widget-tweaks`.
=== 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 http://django-crispy-forms.readthedocs.io/en/latest/[Django-Crispy-Forms]. Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le *layout* et la présentation.
(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
=== En conclusion
. Toute donnée entrée par l'utilisateur **doit** passer par une instance de `form`.
. euh ?

View File

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

View File

@ -1,404 +0,0 @@
== Modélisation
Ce chapitre aborde la modélisation des objets et les options qui y sont liées.
Avec Django, la modélisation est en lien direct avec la conception et le stockage, sous forme d'une base de données relationnelle, et la manière dont ces données s'agencent et communiquent entre elles.
Cette modélisation va ériger les premières pierres de votre édifice
[quote,Aurélie Jean,De l'autre côté de la machine,Une rencontre sans fin]
_Le modèle n'est qu'une grande hypothèque.
Il se base sur des choix conscients et inconscients, et dans chacun de ces choix se cachent nos propres perceptions qui résultent de qui nous sommes, de nos connaissances, de nos profils scientifiques et de tant d'autres choses._
Comme expliqué par Aurélie Jean cite:[other_side], "_toute modélisation reste une approximation de la réalité_".
Plus tard dans ce chapitre, nous expliquerons les bonnes pratiques à suivre pour faire évoluer ces biais.
Django utilise un paradigme de persistence des données de type https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[ORM] - c'est-à-dire que chaque type d'objet manipulé peut s'apparenter à une table SQL, tout en respectant une approche propre à la programmation orientée object.
Plus spécifiquement, l'ORM de Django suit le patron de conception https://en.wikipedia.org/wiki/Active_record_pattern[Active Records], comme le font par exemple https://rubyonrails.org/[Rails] pour Ruby ou https://docs.microsoft.com/fr-fr/ef/[EntityFramework] pour .Net.
Le modèle de données de Django est sans doute la (seule ?) partie qui soit tellement couplée au framework qu'un changement à ce niveau nécessitera une refonte complète de beaucoup d'autres briques de vos applications; là où un pattern de type https://www.martinfowler.com/eaaCatalog/repository.html[Repository] permettrait justement de découpler le modèle des données de l'accès à ces mêmes données, un pattern Active Record lie de manière extrêmement forte le modèle à sa persistence.
Architecturalement, c'est sans doute la plus grosse faiblesse de Django, à tel point que *ne pas utiliser cette brique de fonctionnalités* peut remettre en question le choix du framework.
Conceptuellement, c'est pourtant la manière de faire qui permettra d'avoir quelque chose à présenter très rapidement: à partir du moment où vous aurez un modèle de données, vous aurez accès, grâce à cet ORM à:
1. Des migrations de données et la possibilité de faire évoluer votre modèle,
2. Une abstraction entre votre modélisation et la manière dont les données sont représentées _via_ un moteur de base de données relationnelles,
3. Une interface d'administration auto-générée
4. Un mécanisme de formulaires HTML qui soit complet, pratique à utiliser, orienté objet et facile à faire évoluer,
5. Une définition des notions d'héritage (tout en restant dans une forme d'héritage simple).
Comme tout ceci reste au niveau du code, cela suit également la méthodologie des douze facteurs, concernant la minimisation des divergences entre environnements d'exécution: comme tout se trouve au niveau du code, il n'est plus nécessaire d'avoir un DBA qui doive démarrer un script sur un serveur au moment de la mise à jour, de recevoir une release note de 512 pages en PDF reprenant les modifications ou de nécessiter l'intervention de trois équipes différentes lors d'une modification majeure du code.
Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande.
=== Active Records
Il est important de noter que l'implémentation d'Active Records reste une forme hybride entre une structure de données brutes et une classe:
* Une classe va exposer ses données derrière une forme d'abstraction et n'exposer que les fonctions qui opèrent sur ces données,
* Une structure de données ne va exposer que ses champs et propriétés, et ne va pas avoir de functions significatives.
L'exemple ci-dessous présente trois structure de données, qui exposent chacune leurs propres champs:
[source,python]
----
class Square:
def __init__(self, top_left, side):
self.top_left = top_left
self.side = side
class Rectangle:
def __init__(self, top_left, height, width):
self.top_left = top_left
self.height = height
self.width = width
class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius
----
Si nous souhaitons ajouter une fonctionnalité permettant de calculer l'aire pour chacune de ces structures, nous aurons deux possibilités:
1. Soit ajouter une classe de _visite_ qui ajoute cette fonction de calcul d'aire
2. Soit modifier notre modèle pour que chaque structure hérite d'une classe de type `Shape`, qui implémentera elle-même ce calcul d'aire.
Dans le premier cas, nous pouvons procéder de la manière suivante:
[source,python]
----
class Geometry:
PI = 3.141592653589793
def area(self, shape):
if isinstance(shape, Square):
return shape.side * shape.side
if isinstance(shape, Rectangle):
return shape.height * shape.width
if isinstance(shape, Circle):
return PI * shape.radius**2
raise NoSuchShapeException()
----
Dans le second cas, l'implémentation pourrait évoluer de la manière suivante:
[source,python]
----
class Shape:
def area(self):
pass
class Square(Shape):
def __init__(self, top_left, side):
self.__top_left = top_left
self.__side = side
def area(self):
return self.__side * self.__side
class Rectangle(Shape):
def __init__(self, top_left, height, width):
self.__top_left = top_left
self.__height = height
self.__width = width
def area(self):
return self.__height * self.__width
class Circle(Shape):
def __init__(self, center, radius):
self.__center = center
self.__radius = radius
def area(self):
PI = 3.141592653589793
return PI * self.__radius**2
----
Une structure de données peut être rendue abstraite au travers des notions de programmation orientée objet.
Dans l'exemple géométrique ci-dessus, repris de cite:[clean_code(95-97)], l'accessibilité des champs devient restreinte, tandis que la fonction `area()` bascule comme méthode d'instance plutôt que de l'isoler au niveau d'un visiteur.
Nous ajoutons une abstraction au niveau des formes grâce à un héritage sur la classe `Shape`; indépendamment de ce que nous manipulerons, nous aurons la possibilité de calculer son aire.
Une structure de données permet de facilement gérer des champs et des propriétés, tandis qu'une classe gère et facilite l'ajout de fonctions et de méthodes.
Le problème d'Active Records est que chaque classe s'apparente à une table SQL et revient donc à gérer des _DTO_ ou _Data Transfer Object_, c'est-à-dire des objets de correspondance pure et simple entre les champs de la base de données et les propriétés de la programmation orientée objet, c'est-à-dire également des classes sans fonctions.
Or, chaque classe a également la possibilité d'exposer des possibilités d'interactions au niveau de la persistence, en https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.save[enregistrant ses propres données] ou en en autorisant leur https://docs.djangoproject.com/en/stable/ref/models/instances/#deleting-objects[suppression].
Nous arrivons alors à un modèle hybride, mélangeant des structures de données et des classes d'abstraction, ce qui restera parfaitement viable tant que l'on garde ces principes en tête et que l'on se prépare à une éventuelle réécriture du code.
Lors de l'analyse d'une classe de modèle, nous pouvons voir que Django exige un héritage de la classe `django.db.models.Model`.
Nous pouvons regarder les propriétés définies dans cette classe en analysant le fichier `lib\site-packages\django\models\base.py`.
Outre que `models.Model` hérite de `ModelBase` au travers de https://pypi.python.org/pypi/six[six] pour la rétrocompatibilité vers Python 2.7, cet héritage apporte notamment les fonctions `save()`, `clean()`, `delete()`, ...
En résumé, toutes les méthodes qui font qu'une instance sait **comment** interagir avec la base de données.
=== Types de champs, relations et clés étrangères
Nous l'avons vu plus tôt, Python est un langage dynamique et fortement typé.
Django, de son côté, ajoute une couche de typage statique exigé par le lien sous-jacent avec le moteur de base de données relationnelle.
Dans le domaine des bases de données relationnelles, un point d'attention est de toujours disposer d'une clé primaire pour nos enregistrements.
Si aucune clé primaire n'est spécifiée, Django s'occupera d'en ajouter une automatiquement et la nommera (par convention) `id`.
Elle sera ainsi accessible autant par cette propriété que par la propriété `pk`.
Chaque champ du modèle est donc typé et lié, soit à une primitive, soit à une autre instance au travers de sa clé d'identification.
Grâce à toutes ces informations, nous sommes en mesure de représenter facilement des livres liés à des catégories:
[source,python]
----
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
----
Par défaut, et si aucune propriété ne dispose d'un attribut `primary_key=True`, Django s'occupera d'ajouter un champ `id` grâce à son héritage de la classe `models.Model`.
Les autres champs nous permettent d'identifier une catégorie (`Category`) par un nom (`name`), tandis qu'un livre (`Book`) le sera par ses propriétés `title` et une clé de relation vers une catégorie.
Un livre est donc lié à une catégorie, tandis qu'une catégorie est associée à plusieurs livres.
image::diagrams/books-foreign-keys-example.drawio.png[]
A présent que notre structure dispose de sa modélisation, il nous faut informer le moteur de base de données de créer la structure correspondance:
[source,shell]
----
$ python manage.py makemigrations
Migrations for 'library':
library/migrations/0001_initial.py
- Create model Category
- Create model Book
----
Cette étape créera un fichier différentiel, explicitant les modifications à appliquer à la structure de données pour rester en corrélation avec la modélisation de notre application.
Nous pouvons écrire un premier code d'initialisation de la manière suivante:
[source,python]
----
from library.models import Book, Category
movies = Category.objects.create(name="Adaptations au cinéma")
medieval = Category.objects.create(name="Médiéval-Fantastique")
science_fiction = Category.objects.create(name="Sciences-fiction")
computers = Category.objects.create(name="Sciences Informatiques")
books = {
"Harry Potter": movies,
"The Great Gatsby": movies,
"Dune": science_fiction,
"H2G2": science_fiction,
"Ender's Game": science_fiction,
"Le seigneur des anneaux": medieval,
"L'Assassin Royal", medieval,
"Clean code": computers,
"Designing Data-Intensive Applications": computers
}
for book_title, category in books.items:
Book.objects.create(name=book_title, category=category)
----
Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs catégories:
* _Dune_ a été adapté au cinéma en 1973 et en 2021, de même que _Le Seigneur des Anneaux_. Ces deux titres (au moins) peuvent appartenir à deux catégories distinctes.
* Pour _The Great Gatsby_, c'est l'inverse: nous l'avons initialement classé comme film, mais le livre existe depuis 1925.
* Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie spéciale "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga _Harry Potter_ et ses dérivés.
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour qu'une occurrence puisse être liée à plusieurs catégories.
Au lieu d'utiliser un champ de type `ForeignKey`, nous utiliserons un champ de type `ManyToMany`, c'est-à-dire qu'à présent, un livre pourra être lié à plusieurs catégories, et qu'inversément, une même catégorie pourra être liée à plusieurs livres.
[source,python,highlight=6]
----
class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category, on_delete=models.CASCADE)
----
Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition.
==== Accès aux relations
[source,python]
----
# wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist)
----
Depuis le code, à partir de l'instance de la classe `Item`, on peut donc accéder à la liste en appelant la propriété `wishlist` de notre instance. *A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `<nom de la propriété>_set`; ici `item_set`.
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut `related_name` afin de nommer la relation inverse.
[source,python]
----
# wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist, related_name='items')
----
NOTE: Si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse.
Pour palier à ce problème, nous fixons une valeur à l'attribut `related_name`.
Par facilité (et par conventions), prenez l'habitude de toujours ajouter cet attribut: votre modèle gagnera en cohérence et en lisibilité.
Si cette relation inverse n'est pas nécessaire, il est possible de l'indiquer (par convention) au travers de l'attribut `related_name="+"`.
A partir de maintenant, nous pouvons accéder à nos propriétés de la manière suivante:
[source,python]
----
# python manage.py shell
>>> from wish.models import Wishlist, Item
>>> wishlist = Wishlist.create('Liste de test', 'description')
>>> item = Item.create('Element de test', 'description', w)
>>>
>>> item.wishlist
<Wishlist: Wishlist object>
>>>
>>> wishlist.items.all()
[<Item: Item object>]
----
==== N+1 Queries
=== Unicité
=== Indices
==== Conclusions
Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (**ForeignKey**) d'une classe A vers une classe B.
Pour représenter d'autres types de relations, il existe également les champs de type *ManyToManyField*, afin de représenter une relation N-N.
Il existe également un type de champ spécial pour les clés étrangères, qui est le Les champs de type *OneToOneField*, pour représenter une relation 1-1.
==== Metamodèle et introspection
Comme chaque classe héritant de `models.Model` possède une propriété `objects`. Comme on l'a vu dans la section **Jouons un peu avec la console**, cette propriété permet d'accéder aux objects persistants dans la base de données, au travers d'un `ModelManager`.
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.
[source,python,highlight=5]
----
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',) <1>
----
<1> Nous définissons un ordre par défaut, directement au niveau du modèle. Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre (la méthode `order_by` existe et peut être chaînée à n'importe quel _queryset_). D'où l'intérêt de tester ce type de comportement, dans la mesure où un `top 1` dans votre code pourrait être modifié simplement par cette petite information.
Pour sélectionner un objet au pif : `return Category.objects.order_by("?").first()`
Les propriétés de la classe Meta les plus utiles sont les suivates:
* `ordering` pour spécifier un ordre de récupération spécifique.
* `verbose_name` pour indiquer le nom à utiliser au singulier pour définir votre classe
* `verbose_name_plural`, pour le pluriel.
* `contraints` (Voir https://girlthatlovestocode.com/django-model[ici]-), par exemple
[source,python]
----
constraints = [ # constraints added
models.CheckConstraint(check=models.Q(year_born__lte=datetime.date.today().year-18), name='will_be_of_age'),
]
----
==== Choix
Voir https://girlthatlovestocode.com/django-model[ici]
[source,python]
----
class Runner(models.Model):
# this is new:
class Zone(models.IntegerChoices):
ZONE_1 = 1, 'Less than 3.10'
ZONE_2 = 2, 'Less than 3.25'
ZONE_3 = 3, 'Less than 3.45'
ZONE_4 = 4, 'Less than 4 hours'
ZONE_5 = 5, 'More than 4 hours'
name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField()
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?") # this is new
----
==== Validateurs
==== Constructeurs
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode `__init__`: créez plutôt une méthode static de type `create()`, en y associant les paramètres obligatoires ou souhaités:
[source,python]
----
class Wishlist(models.Model):
@staticmethod
def create(name, description):
w = Wishlist()
w.name = name
w.description = description
w.save()
return w
class Item(models.Model):
@staticmethod
def create(name, description, wishlist):
i = Item()
i.name = name
i.description = description
i.wishlist = wishlist
i.save()
return i
----
Mieux encore: on pourrait passer par un `ModelManager` pour limiter le couplage;
l'accès à une information stockée en base de données ne se ferait dès lors qu'au travers
de cette instance et pas directement au travers du modèle. De cette manière, on limite le
couplage des classes et on centralise l'accès.
[source,python]
----
class ItemManager(...):
(de mémoire, je ne sais plus exactement :-))
----
=== Conclusion
Le modèle proposé par Django est un composant extrêmement performant, mais fort couplé avec le coeur du framework.
Si tous les composants peuvent être échangés avec quelques manipulations, le cas du modèle sera plus difficile à interchanger.
A côté de cela, il permet énormément de choses, et vous fera gagner un temps précieux, tant en rapidité d'essais/erreurs, que de preuves de concept.
Une possibilité peut également être de cantonner Django à un framework

View File

@ -1,129 +0,0 @@
### Querysets & managers
* http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm
* https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator
* http://blog.etianen.com/blog/2013/06/08/django-querysets/
L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants:
* Les `managers`, qui consistent en un point d'entrée pour accéder aux objets persistants
* Les `querysets`, qui permettent de filtrer des ensembles ou sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour ajouter
d'autres filtres à des filtres existants, et fonctionnent comme un super jeu d'abstraction pour accéder à nos données (persistentes).
Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut `objects`, qui correspond
à un manager (ou un gestionnaire, si vous préférez).
Ce gestionnaire constitue l'interface par laquelle vous accéderez à la base de données. Mais pour cela, vous aurez aussi besoin d'appliquer certains requêtes ou filtres. Et pour cela, vous aurez besoin des `querysets`, qui consistent en des ... ensembles de requêtes :-).
Si on veut connaître la requête SQL sous-jacente à l'exécution du queryset, il suffit d'appeler la fonction str() sur la propriété `query`:
[source,python]
----
queryset = Wishlist.objects.all()
print(queryset.query)
----
Conditions AND et OR sur un queryset
Pour un `AND`, il suffit de chaîner les conditions. ** trouver un exemple ici ** :-)
Mais en gros : bidule.objects.filter(condition1, condition2)
Il existe deux autres options : combiner deux querysets avec l'opérateur `&` ou combiner des Q objects avec ce même opérateur.
Soit encore combiner des filtres:
[source,python]
----
from core.models import Wish
Wish.objects <1>
Wish.objects.filter(name__icontains="test").filter(name__icontains="too") <2>
----
<1> Ca, c'est notre manager.
<2> Et là, on chaîne les requêtes pour composer une recherche sur tous les souhaits dont le nom contient (avec une casse insensible) la chaîne "test" et dont le nom contient la chaîne "too".
Pour un 'OR', on a deux options :
. Soit passer par deux querysets, typiuqment `queryset1 | queryset2`
. Soit passer par des `Q objects`, que l'on trouve dans le namespace `django.db.models`.
[source,python]
----
from django.db.models import Q
condition1 = Q(...)
condition2 = Q(...)
bidule.objects.filter(condition1 | condition2)
----
L'opérateur inverse (_NOT_)
Idem que ci-dessus : soit on utilise la méthode `exclude` sur le queryset, soit l'opérateur `~` sur un Q object;
Ajouter les sujets suivants :
. Prefetch
. select_related
==== Gestionnaire de models (managers) et opérations
Chaque définition de modèle utilise un `Manager`, afin d'accéder à la base de données et traiter nos demandes.
Indirectement, une instance de modèle ne *connait* **pas** la base de données: c'est son gestionnaire qui a cette tâche.
Il existe deux exceptions à cette règle: les méthodes `save()` et `update()`.
* Instanciation: MyClass()
* Récupération: MyClass.objects.get(pk=...)
* Sauvegarde : MyClass().save()
* Création: MyClass.objects.create(...)
* Liste des enregistrements: MyClass.objects.all()
Par défaut, le gestionnaire est accessible au travers de la propriété `objects`.
Cette propriété a une double utilité:
1. Elle est facile à surcharger - il nous suffit de définir une nouvelle classe héritant de ModelManager, puis de définir, au niveau de la classe, une nouvelle assignation à la propriété `objects`
2. Il est tout aussi facile de définir d'autres propriétés présentant des filtres bien spécifiques.
==== Requêtes
DANGER: Les requêtes sont sensibles à la casse, **même** si le moteur de base de données ne l'est pas.
C'est notamment le cas pour Microsoft SQL Server; faire une recherche directement via les outils de Microsoft ne retournera pas
obligatoirement les mêmes résultats que les managers, qui seront beaucoup plus tatillons sur la qualité des recherches par
rapport aux filtres paramétrés en entrée.
==== Jointures
Pour appliquer une jointure sur un modèle, nous pouvons passer par les méthodes `select_related` et `prefetch_related`.
Il faut cependant faire **très** attention au prefetch related, qui fonctionne en fait comme une grosse requête dans laquelle
nous trouvons un `IN (...)`.
Càd que Django va récupérer tous les objets demandés initialement par le queryset, pour ensuite prendre toutes les clés primaires,
pour finalement faire une deuxième requête et récupérer les relations externes.
Au final, si votre premier queryset est relativement grand (nous parlons de 1000 à 2000 éléments, en fonction du moteur de base de données),
la seconde requête va planter et vous obtiendrez une exception de type `django.db.utils.OperationalError: too many SQL variables`.
Nous pourrions penser qu'utiliser un itérateur permettrait de combiner les deux, mais ce n'est pas le cas...
Comme l'indique la documentation:
Note that if you use iterator() to run the query, prefetch_related() calls will be ignored since these two optimizations do not make sense together.
Ajouter un itérateur va en fait forcer le code à parcourir chaque élément de la liste, pour l'évaluer.
Il y aura donc (à nouveau) autant de requêtes qu'il y a d'éléments, ce que nous cherchons à éviter.
[source,python]
----
informations = (
<MyObject>.objects.filter(<my_criteria>)
.select_related(<related_field>)
.prefetch_related(<related_field>)
.iterator(chunk_size=1000)
)
----
=== Aggregate vs. Annotate
https://docs.djangoproject.com/en/3.1/topics/db/aggregation/

View File

@ -1,23 +0,0 @@
[source,python]
----
INSTALLED_APPS = [
"django.contrib..."
]
----
peut être splitté en plusieurs parties:
[source,python]
----
INSTALLED_APPS = [
]
THIRD_PARTIES = [
]
MY_APPS = [
]
----

View File

@ -1 +0,0 @@
== Shell

View File

@ -1,63 +0,0 @@
== Templates
=== Structure et configuration
==== Répertoires de découverte des templates
==== Fichiers statiques
(à compléter)
=== Builtins
[source,python]
----
----
==== Regroup By
(le truc super facile qui sert à mort dans plein de cas sans qu'on n'ait à se casser la tête).
=== Non-builtins
[source,bash]
----
[Inclure un tree du dossier template tags]
----
Pour plus d'informations, la https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#writing-custom-template-tags[documentation officielle est un bon début].
=== Contexts Processors
Un `context processor` permet d'ajouter des informations par défaut à un contexte (le dictionnaire qu'on passe de la vue au template).
L'idée est d'ajouter une fonction à un module Python à notre projet, puis de le référencer parmi
les CONTEXT_PROCESSORS de nos paramètres généraux. Cette fonction doit peupler un dictionnaire, et les clés de ce dictionnaire seront
directement ajoutées à tout autre dictionnaire/contexte passé à une vue. Par exemple:
(cf. https://stackoverflow.com/questions/60515797/default-context-for-all-pages-django[StackOverflow] - à retravailler)
[source,python]
----
----
[source,python]
----
'OPTIONS': {
'context_processors': [
....
....
],
},
----

View File

@ -1,66 +0,0 @@
=== Tests
[quote,Robert C. Martin, Clean Architecture]
Tests are part of the system.
==== Types de tests
Les *tests unitaires* ciblent typiquement une seule fonction, classe ou méthode, de manière isolée, en fournissant au développeur l'assurance que son code réalise ce qu'il en attend. Pour plusieurs raisons (et notamment en raison de performances), les tests unitaires utilisent souvent des données stubbées - pour éviter d'appeler le "vrai" service.
> The aim of a unit test is to show that a single part of the application does what programmer intends it to.
Les *tests d'acceptance* vérifient que l'application fonctionne comme convenu, mais à un plus haut niveau (fonctionnement correct d'une API, validation d'une chaîne d'actions effectuées par un humain, ...).
> The objective of acceptance tests is to prove that our application does what the customer meant it to.
Les *tests d'intégration* vérifient que l'application coopère correctement avec les systèmes périphériques.
De manière plus générale, si nous nous rendons compte que les tests sont trop compliqués à écrire ou nous coûtent trop de temps, c'est sans doute que l'architecture de la solution n'est pas adaptée et que les composants sont couplés les uns aux autres. Dans ces cas, il sera nécessaire de refactoriser le code, afin que chaque module puisse être testé indépendamment des autres. cite:[clean_architecture]
[quote,Robert C. Martin, Clean Architecture]
Martin Fowler observes that, in general, "a ten minute build [and test process] is perfectly within reason...
[We first] do the compilation and run tests that are more localized unit tests with the database completely stubbed out.
Such tests can run very fast, keeping within the ten minutes guideline.
However any bugs that involve larger scale intercations, particularly those involving the real database, won't be found.
The second stage build runs a different suite of tests [acceptance tests] that do hit the real database and involve more end-to-end behavior.
This suite may take a couple of hours to run.
Au final, le plus important est de toujours corréler les phases de tests indépendantes du reste du travail (de développement, ici), en l'automatisant au plus près de sa source de création.
En résumé, il est recommandé de:
1. 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
2. Tester que l'URL envoie bien vers l'exécution d'une fonction (et que cette fonction est celle que l'on attend)
TODO: Voir comment configurer une `memoryDB` pour l'exécution des tests.
==== 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)
----

View File

@ -1,10 +0,0 @@
=== Traductions
[source,bash]
----
python manage.py makemessages
----
+ plein d'explications: traductions, génération, interface (urls, breadcrumbs, ...).
+ naive datetimes

View File

@ -1,178 +0,0 @@
== Vues
Une vue correspond à un contrôleur dans le pattern MVC.
Tout ce que vous pourrez définir au niveau du fichier `views.py` fera le lien entre le modèle stocké dans la base de données
et ce avec quoi l'utilisateur pourra réellement interagir (le `template`).
De manière très basique, une vue consiste en un objet Python dont le retour sera une instance de type `HttpResponse:
[source,python]
----
from django.http import HttpResponse
def home(request):
return HttpResponse("Hello, World!")
----
Techniquement, une vue pourrait retourner autre chose qu'une réponse Http, mais cela bloquera au niveau des middlewares,
qui s'attendent tous à traiter un objet de ce type-là.
Les APIs de type REST, SOAP ou GraphQL ne font finalement qu'une chose: encapsuler leur résultat dans un objet de type HttpResponse.
Le paramètre `request` est lui de type `HttpRequest` et embarque https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest[énormément d'informations],
dont le schéma, les cookies, les verbes http,
NOTE: détailler les informations de l'objet request :-p
Chaque vue peut etre représentée de deux manières:
1. Soit par des fonctions,
2. Soit par des classes.
Le comportement leur est propre, mais le résultat reste identique. Le lien entre l'URL à laquelle l'utilisateur accède et son exécution est faite au travers du fichier `gwift/urls.py`, comme on le verra par la suite.
=== Function Based Views
Les fonctions (ou `FBV` pour *Function Based Views*) permettent une implémentation classique des contrôleurs. Au fur et à mesure de votre implémentation, on se rendra compte qu'il y a beaucoup de répétitions dans ce type d'implémentation: elles ne sont pas obsolètes, mais dans certains cas, il sera préférable de passer par les classes.
Pour définir la liste des `WishLists` actuellement disponibles, on précédera de la manière suivante:
. Définition d'une fonction qui va récupérer les objets de type `WishList` dans notre base de données. La valeur de retour sera la construction d'un dictionnaire (le *contexte*) qui sera passé à un template HTML. On démandera à ce template d'effectuer le rendu au travers de la fonction `render`, qui est importée par défaut dans le fichier `views.py`.
. Construction d'une URL qui permettra de lier l'adresse à l'exécution de la fonction.
. Définition du squelette.
[source,python]
----
# wish/views.py
from django.shortcuts import render
from .models import Wishlist
def wishlists(request):
wishlists = Wishlist.objects.all()
return render(
request,
'wish/list.html',
{
'wishlists': wishlists
}
)
----
Rien qu'ici, on doit déjà tester deux choses:
. Qu'on construit bien le modèle attendu - la liste de tous les souhaits déjà émis.
. Que le template `wish/list.html` existe bien - sans quoi, on va tomber sur une erreur de type `TemplateDoesNotExist` dans notre environnement de test, et sur une erreur 500 en production.
A ce stade, vérifiez que la variable `TEMPLATES` est correctement initialisée dans le fichier `gwift/settings.py` et que le fichier `templates/wish/list.html` ressemble à ceci:
[source,jinj2]
----
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
</body>
</html>
----
A présent, ajoutez quelques listes de souhaits grâce à un *shell*, puis lancez le serveur:
[source,bash]
----
$ python manage.py shell
>>> from wish.models import Wishlist
>>> Wishlist.create('Décembre', "Ma liste pour les fêtes de fin d'année")
<Wishlist: Wishlist object>
>>> Wishlist.create('Anniv 30 ans', "Je suis vieux! Faites des dons!")
<Wishlist: Wishlist object>
----
Lancez le serveur grâce à la commande `python manage.py runserver`, ouvrez un navigateur quelconque et rendez-vous à l'adresse `http://localhost:8000 <http://localhost:8000>`_. Vous devriez obtenir le résultat suivant:
.. image:: mvc/my-first-wishlists.png
:align: center
Rien de très sexy, aucune interaction avec l'utilisateur, très peu d'utilisation des variables contextuelles, mais c'est un bon début! =)
=== Class Based Views
Les classes, de leur côté, implémente le *pattern* objet et permettent d'arriver facilement à un résultat en très peu de temps, parfois même en définissant simplement quelques attributs, et rien d'autre. Pour l'exemple, on va définir deux classes qui donnent exactement le même résultat que la fonction `wishlists` ci-dessus. Une première fois en utilisant une classe générique vierge, et ensuite en utilisant une classe de type `ListView`.
Voir https://ccbv.co.uk/[Classy Class Based Views].
L'idée derrière les classes est de définir des fonctions *par convention plutôt que par configuration*.
NOTE: à compléter ici :-)
==== ListView
Les classes génériques implémentent un aspect bien particulier de la représentation d'un modèle, en utilisant très peu d'attributs. Les principales classes génériques sont de type `ListView`, [...]. L'implémentation consiste, exactement comme pour les fonctions, à:
. Définir une sous-classe de celle que l'on souhaite utiliser
. Câbler l'URL qui lui sera associée
. Définir le squelette.
[source,python]
----
# wish/views.py
from django.views.generic import ListView
from .models import Wishlist
class WishListList(ListView):
context_object_name = 'wishlists'
model = Wishlist
template_name = 'wish/list.html'
----
Il est même possible de réduire encore ce morceau de code en définissant juste le snippet suivant :
[source,python]
----
# wish/views.py
from django.views.generic import ListView
from .models import Wishlist
class WishListList(ListView):
context_object_name = 'wishlists'
----
Par inférence, Django construit beaucoup d'informations: si on n'avait pas spécifié les variables `context_object_name` et `template_name`, celles-ci auraient pris les valeurs suivantes:
* `context_object_name`: `wishlist_list` (ou plus précisément, le nom du modèle suivi de `_list`)
* `template_name`: `wish/wishlist_list.html` (à nouveau, le fichier généré est préfixé du nom du modèle).
En l'état, par rapport à notre précédente vue basée sur une fonction, on y gagne sur les conventions utilisées et le nombre de tests à réaliser. A vous de voir la déclaration que vous préférez, en fonction de vos affinités et du résultat que vous souhaitez atteindre.
NOTE: un petit tableau de différence entre les deux ? :-)
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]
----
C'est tout. Lancez le serveur, le résultat sera identique.

View File

@ -1,18 +0,0 @@
= Services Oriented Applications
Nous avons fait exprès de reprendre l'acronyme d'une _Services Oriented Architecture_ pour cette partie.
L'objectif est de vous mettre la puce à l'oreille quant à la finalité du développement: que l'utilisateur soit humain, bot automatique ou client Web, l'objectif est de fournir des applications résilientes, disponibles et accessibles.
Dans cette partie, nous aborderons les vues, la mise en forme, la mise en page, la définition d'une interface REST, la définition d'une interface GraphQL et le routage d'URLs.
include::rest.adoc[]
include::urls.adoc[]
include::localization.adoc[]
include::trees.adoc[]
== Conclusions
De part son pattern `MVT`, Django ne fait pas comme les autres frameworks.

View File

@ -1,385 +0,0 @@
== Application Programming Interface
NOTE: https://news.ycombinator.com/item?id=30221016&utm_term=comment vs Django Rest Framework
NOTE: Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis.
NOTE: Voir peut-être aussi https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/
Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service d'affectation.
Quelque chose comme ceci:
[source,python]
----
# models.py
from django.db import models
class People(models.Model):
CIVILITY_CHOICES = (
("M", "Monsieur"),
("Mme", "Madame"),
("Dr", "Docteur"),
("Pr", "Professeur"),
("", "")
)
last_name = models.CharField(max_length=255)
first_name = models.CharField(max_length=255)
civility = models.CharField(
max_length=3,
choices=CIVILITY_CHOICES,
default=""
)
def __str__(self):
return "{}, {}".format(self.last_name, self.first_name)
class Service(models.Model):
label = models.CharField(max_length=255)
def __str__(self):
return self.label
class ContractType(models.Model):
label = models.CharField(max_length=255)
short_label = models.CharField(max_length=50)
def __str__(self):
return self.short_label
class Contract(models.Model):
people = models.ForeignKey(People, on_delete=models.CASCADE)
date_begin = models.DateField()
date_end = models.DateField(blank=True, null=True)
contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE)
service = models.ForeignKey(Service, on_delete=models.CASCADE)
def __str__(self):
if self.date_end is not None:
return "A partir du {}, jusqu'au {}, dans le service {} ({})".format(
self.date_begin,
self.date_end,
self.service,
self.contract_type
)
return "A partir du {}, à durée indéterminée, dans le service {} ({})".format(
self.date_begin,
self.service,
self.contract_type
)
----
image::images/rest/models.png[]
## Configuration
La configuration des points de terminaison de notre API est relativement touffue.
Il convient de:
1. Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de l'API,
2. Configurer les vues, càd le comportement de chacun des points de terminaison,
3. Configurer les points de terminaison eux-mêmes, càd les URLs permettant d'accéder aux ressources.
4. Et finalement ajouter quelques paramètres au niveau de notre application.
### Sérialiseurs
```python
# serializers.py
from django.contrib.auth.models import User, Group
from rest_framework import serializers
from .models import People, Contract, Service
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class ServiceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Service
fields = ("name",)
```
### Vues
```python
# views.py
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from rest_framework import permissions
from .models import People, Contract, Service
from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer
class PeopleViewSet(viewsets.ModelViewSet):
queryset = People.objects.all()
serializer_class = PeopleSerializer
permission_class = [permissions.IsAuthenticated]
class ContractViewSet(viewsets.ModelViewSet):
queryset = Contract.objects.all()
serializer_class = ContractSerializer
permission_class = [permissions.IsAuthenticated]
class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all()
serializer_class = ServiceSerializer
permission_class = [permissions.IsAuthenticated]
```
### URLs
```python
# urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from core import views
router = routers.DefaultRouter()
router.register(r"people", views.PeopleViewSet)
router.register(r"contracts", views.ContractViewSet)
router.register(r"services", views.ServiceViewSet)
urlpatterns = [
path("api/v1/", include(router.urls)),
path('admin/', admin.site.urls),
]
```
### Paramètres
```python
# settings.py
INSTALLED_APPS = [
...
"rest_framework",
...
]
...
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
```
A ce stade, en nous rendant sur l'URL `http://localhost:8000/api/v1`, nous obtiendrons ceci:
image::images/rest/api-first-example.png[]
## Modèles et relations
Plus haut, nous avons utilisé une relation de type `HyperlinkedModelSerializer`.
C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité.
Pour palier à ceci, il existe [plusieurs manières de représenter ces relations](https://www.django-rest-framework.org/api-guide/relations/): soit *via* un hyperlien, comme ci-dessus, soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource.
La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats):
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
"http://localhost:8000/api/v1/contracts/1/",
"http://localhost:8000/api/v1/contracts/2/"
]
}
]
}
```
à ceci:
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
{
"date_begin": "2019-01-01",
"date_end": null,
"service": "http://localhost:8000/api/v1/services/1/"
},
{
"date_begin": "2009-01-01",
"date_end": "2021-01-01",
"service": "http://localhost:8000/api/v1/services/1/"
}
]
}
]
}
```
La modification se limite à **surcharger** la propriété, pour indiquer qu'elle consiste en une instance d'un des sérialiseurs existants.
Nous passons ainsi de ceci
```python
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
```
à ceci:
```python
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
contract_set = ContractSerializer(many=True, read_only=True)
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
```
Nous ne faisons donc bien que redéfinir la propriété `contract_set` et indiquons qu'il s'agit à présent d'une instance de `ContractSerializer`, et qu'il est possible d'en avoir plusieurs.
C'est tout.
## Filtres et recherches
A ce stade, nous pouvons juste récupérer des informations présentes dans notre base de données, mais à part les parcourir, il est difficile d'en faire quelque chose.
Il est possible de jouer avec les URLs en définissant une nouvelle route ou avec les paramètres de l'URL, ce qui demanderait alors de programmer chaque cas possible - sans que le consommateur ne puisse les déduire lui-même. Une solution élégante consiste à autoriser le consommateur à filtrer les données, directement au niveau de l'API.
Ceci peut être fait. Il existe deux manières de restreindre l'ensemble des résultats retournés:
1. Soit au travers d'une recherche, qui permet d'effectuer une recherche textuelle, globale et par ensemble à un ensemble de champs,
2. Soit au travers d'un filtre, ce qui permet de spécifier une valeur précise à rechercher.
Dans notre exemple, la première possibilité sera utile pour rechercher une personne répondant à un ensemble de critères. Typiquement, `/api/v1/people/?search=raymond bond` ne nous donnera aucun résultat, alors que `/api/v1/people/?search=james bond` nous donnera le célèbre agent secret (qui a bien entendu un contrat chez nous...).
Le second cas permettra par contre de préciser que nous souhaitons disposer de toutes les personnes dont le contrat est ultérieur à une date particulière.
Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de proposer immédiatement les champs, et donc d'informer le consommateur des possibilités:
image::images/rest/drf-filters-and-searches.png[]
### Recherches
La fonction de recherche est déjà implémentée au niveau de Django-Rest-Framework, et aucune dépendance supplémentaire n'est nécessaire.
Au niveau du `viewset`, il suffit d'ajouter deux informations:
```python
...
from rest_framework import filters, viewsets
...
class PeopleViewSet(viewsets.ModelViewSet):
...
filter_backends = [filters.SearchFilter]
search_fields = ["last_name", "first_name"]
...
```
### Filtres
Nous commençons par installer [le paquet `django-filter`](https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend) et nous l'ajoutons parmi les applications installées:
```bash
λ pip install django-filter
Collecting django-filter
Downloading django_filter-2.4.0-py3-none-any.whl (73 kB)
|████████████████████████████████| 73 kB 2.6 MB/s
Requirement already satisfied: Django>=2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from django-filter) (3.1.7)
Requirement already satisfied: asgiref<4,>=3.2.10 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (3.3.1)
Requirement already satisfied: sqlparse>=0.2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (0.4.1)
Requirement already satisfied: pytz in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (2021.1)
Installing collected packages: django-filter
Successfully installed django-filter-2.4.0
```
Une fois l'installée réalisée, il reste deux choses à faire:
1. Ajouter `django_filters` parmi les applications installées:
2. Configurer la clé `DEFAULT_FILTER_BACKENDS` à la valeur `['django_filters.rest_framework.DjangoFilterBackend']`.
Vous avez suivi les étapes ci-dessus, il suffit d'adapter le fichier `settings.py` de la manière suivante:
```python
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}
```
Au niveau du viewset, il convient d'ajouter ceci:
```python
...
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
...
class PeopleViewSet(viewsets.ModelViewSet):
...
filter_backends = [DjangoFilterBackend]
filterset_fields = ('last_name',)
...
```
A ce stade, nous avons deux problèmes:
1. Le champ que nous avons défini au niveau de la propriété `filterset_fields` exige une correspondance exacte. Ainsi, `/api/v1/people/?last_name=Bon` ne retourne rien, alors que `/api/v1/people/?last_name=Bond` nous donnera notre agent secret préféré.
2. Il n'est pas possible d'aller appliquer un critère de sélection sur la propriété d'une relation. Notre exemple proposant rechercher uniquement les relations dans le futur (ou dans le passé) tombe à l'eau.
Pour ces deux points, nous allons définir un nouveau filtre, en surchargeant une nouvelle classe dont la classe mère serait de type `django_filters.FilterSet`.
TO BE CONTINUED.
A noter qu'il existe un paquet [Django-Rest-Framework-filters](https://github.com/philipn/django-rest-framework-filters), mais il est déprécié depuis Django 3.0, puisqu'il se base sur `django.utils.six` qui n'existe à présent plus. Il faut donc le faire à la main (ou patcher le paquet...).

View File

@ -1,28 +0,0 @@
== Monitoring et activités
[quote,Terry Pratchett,Les Anales du Disque-Monde]
Les magiciens du Disque-Monde ont, de leur côté, calculé que les chances uniques sur un million se produisent 9 fois sur 10.
[source,python]
----
SENTRY_DSN = env("SENTRY_DSN", default=None)
if SENTRY_DSN is not None:
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration()],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
# If you wish to associate users to errors (assuming you are using
# django.contrib.auth) you may enable sending PII data.
send_default_pii=True,
ca_certs=<path_to_pem_file>,
)
----

View File

@ -1,43 +0,0 @@
=== Arborescences
[source,python]
----
----
[source,python]
----
# <app>/management/commands/rebuild.py
"""This command manages Closure Tables implementation
It adds new levels and cleans links between entities.
This way, it's relatively easy to fetch an entire tree with just one tiny request.
"""
from django.core.management.base import BaseCommand
from rps.structure.models import Entity, EntityTreePath
class Command(BaseCommand):
def handle(self, *args, **options):
entities = Entity.objects.all()
for entity in entities:
breadcrumb = [node for node in entity.breadcrumb()]
tree = set(EntityTreePath.objects.filter(descendant=entity))
for idx, node in enumerate(breadcrumb):
tree_path, _ = EntityTreePath.objects.get_or_create(
ancestor=node, descendant=entity, weight=idx + 1
)
if tree_path in tree:
tree.remove(tree_path)
for tree_path in tree:
tree_path.delete()
----

View File

@ -1,91 +0,0 @@
== Tests unitaires
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, on devrait arriver à quelque chose de synthéthique:
* Au niveau de l'infrastructure,
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. la requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. elle est ensuite transmise par socket et est prise en compte par Gunicorn
. qui la transmet ensuite à l'un de ses _workers_ (= un processus Python)
. après exécution, une réponse est renvoyée à l'utilisateur.
image::images/diagrams/architecture.png[]
* Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
. et renvoyer une réponse à l'utilisateur.
image::images/diagrams/django-process.png[]
En gros, ça peut planter aux points suivants :
. L'utilisateur n'est pas connecté à l'Internet
. La version du navigateur utilisé est obsolète et les technologies de sécurité ne sont plus supportées
. La version d'HTTP n'est pas prise en charge par notre serveur
. Le verbe HTTP n'est pas pris en charge par la fonction
. Le système n'a plus de place sur son disque
. Nginx est mal configuré
. La communication par socket est mal configurée
. L'URL est inconnue
. La route n'existe pas
. La base de dnnées est inaccessible
. La fonction n'est pas correctement appelée
. Il manque des valeurs au rendu
. Il y a tellement de données qu'Nginx ou Gunicorn ou déjà envoyé une fin de requête à l'utilisateur (_timeout_)
...
En bref, on a potentiellement un ou plusieurs problèmes potentiels à chaque intervenant. Une chose à la fois: dans un premier temps, on va se concentrer sur notre code.
Vous aurez remarqué ci-dessus qu'une nouvelle application créée par Django vient d'office avec un fichier `tests.py`. C'est simplement parce que les tests unitaires et tests d'intégration font partie intégrante du cadre de travail. Ceci dit, chaque batterie de tests demande une certaine préparation et un certain "temps de cuisson"... Comprendre qu'un test ne sera effectif que s'il est correctement structurée (_configuration over convention_ ?), s'il se trouve dans une classe et que chaque test se trouve bien dans une méthode de cette classe... Cela commence à faire beaucoup de boulot pour vérifier qu'une fonction retourne bien une certaine valeur, et en décourager la plupart d'entrée de jeux, surtout quand on sait que chaque fonction, condition ou point d'entrée sera sensée disposer de son test.
Au niveau des améliorations, on va :
. Changer de framework de test et utiliser https://docs.pytest.org/en/latest/[pytest] et son greffon https://pytest-django.readthedocs.io/en/latest/[pytest-django]
. Ajouter une couverture de code.
=== Pytest
Pourquoi pytest, plutôt que Django ? Par pure fainéantise :-) Si, si ! Pytest est relativement plus facile à utiliser, permet un contrôle plus fin de certaines variables d'environnement et est surtout compatible hors-Django (en plus de quelques améliorations sur les performances, comme les tests sur plusieurs processus).
Cela signifie surtout que si vous apprenez Pytest maintenant, et que votre prochain projet est une application en CLI ou avec un autre framework, vous ne serez pas dépaysé. Les tests unitaires de Django sont compatibles uniquement avec Django... D'où une certaine perte de vélocité lorsqu'on devra s'en détacher.
Vous trouverez ci-dessous une comparaison entre des tests avec les deux frameworks:
[source,python]
----
# avec django
----
[source,python]
----
# avec pytest
----
Plugins pytest :
* https://github.com/CFMTech/pytest-monitor
* https://pivotfinland.com/pytest-sugar/[pytest-sugar]
=== Fixtures
https://realpython.com/django-pytest-fixtures/[Lien]: super bien expliqué, et pourquoi les fixtures dans Pytest c'est 'achement plus mieux que les tests unitaires de Django.
=== Couverture de code
Dans un premier temps, *le pourcentage de code couvert par nos tests*. Une fois ce pourcentage évalué, le but du jeu va consister à *ce que ce pourcentage reste stable ou augmente*. Si vous modifiez une ligne de code et que la couverture passe de 73% à 72%, vous avez perdu et vous devez faire en sorte de corriger.
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,104 +0,0 @@
== URLs et espaces de noms
La gestion des URLs permet *grosso modo* d'assigner une adresse paramétrée ou non à une fonction Python. La manière simple consiste à modifier le fichier `gwift/settings.py` pour y ajouter nos correspondances. Par défaut, le fichier ressemble à ceci:
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]
----
La variable `urlpatterns` associe un ensemble d'adresses à des fonctions.
Dans le fichier *nu*, seul le *pattern* `admin` est défini, et inclut toutes les adresses qui sont définies dans le
fichier `admin.site.urls`.
Django fonctionne avec des *expressions rationnelles* simplifiées (des **expressions régulières** ou **regex**)
pour trouver une correspondance entre une URL et la fonction qui recevra la requête et retournera une réponse.
Nous utilisons l'expression `^$` pour déterminer la racine de notre application, mais nous pourrions appliquer d'autres regroupements
(`/home`, `users/<profile_id>`, `articles/<year>/<month>/<day>`, ...).
Chaque *variable* déclarée dans l'expression régulière sera apparenté à un paramètre dans la fonction correspondante.
Ainsi,
[source,python]
----
# admin.site.urls.py
----
Pour reprendre l'exemple où on en était resté:
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from wish import views as wish_views
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', wish_views.wishlists, name='wishlists'),
]
----
TIP: Dans la mesure du possible, essayez toujours de **nommer** chaque expression.
Cela permettra notamment de les retrouver au travers de la fonction `reverse`, mais permettra également de simplifier vos templates.
A présent, on doit tester que l'URL racine de notre application mène bien vers la fonction `wish_views.wishlists`.
Sauf que les pages `about` et `help` existent également.
Pour implémenter ce type de précédence, il faudrait implémenter les URLs de la manière suivante:
[source,text]
----
| about
| help
| <user>
----
Mais cela signifie aussi que les utilisateurs `about` et `help` (s'ils existent...) ne pourront jamais accéder à leur profil.
Une dernière solution serait de maintenir une liste d'authorité des noms d'utilisateur qu'il n'est pas possible d'utiliser.
D'où l'importance de bien définir la séquence de déinition de ces routes, ainsi que des espaces de noms.
Note sur les namespaces.
De là, découle une autre bonne pratique: l'utilisation de _breadcrumbs_ (https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template) ou de guidelines de navigation.
=== Reverse
En associant un nom ou un libellé à chaque URL, il est possible de récupérer sa *traduction*. Cela implique par contre de ne plus toucher à ce libellé par la suite...
Dans le fichier `urls.py`, on associe le libellé `wishlists` à l'URL `r'^$` (c'est-à-dire la racine du site):
[source,python]
----
from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]
----
De cette manière, dans nos templates, on peut à présent construire un lien vers la racine avec le tags suivant:
[source,html]
----
<a href="{% url 'wishlists' %}">{{ yearvar }} Archive</a>
----
De la même manière, on peut également récupérer l'URL de destination pour n'importe quel libellé, de la manière suivante:
[source,python]
----
from django.core.urlresolvers import reverse_lazy
wishlists_url = reverse_lazy('wishlists')
----

View File

@ -1,19 +0,0 @@
= Go Live !
Pour commencer, nous allons nous concentrer sur la création d'un site ne contenant qu'une seule application, même si en pratique le site contiendra déjà plusieurs applications fournies pas django, comme nous le verrons plus loin.
NOTE: Don't make me think, or why I switched from JS SPAs to Ruby On Rails https://news.ycombinator.com/item?id=30206989&utm_term=comment
Pour prendre un exemple concret, nous allons créer un site permettant de gérer des listes de souhaits, que nous appellerons `gwift` (pour `GiFTs and WIshlisTs` :)).
La première chose à faire est de définir nos besoins du point de vue de l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur puisse faire avec l'application.
Ensuite, nous pourrons traduire ces besoins en fonctionnalités et finalement effectuer le développement.
include::gwift/_main.adoc[]
include::khana/_main.adoc[]
include::legacy/_main.adoc[]

View File

@ -1,14 +0,0 @@
== Gwift
.Gwift
image::images/django/django-project-vs-apps-gwift.png[]
include::specs.adoc[]
include::models.adoc[]
include::tests.adoc[]
include::refactoring.adoc[]
include::user-management.adoc[]

View File

@ -1,178 +0,0 @@
************
Modélisation
************
L'ORM de Django permet de travailler uniquement avec une définition de classes, et de faire en sorte que le lien avec la base de données soit géré uniquement de manière indirecte, par Django lui-même. On peut schématiser ce comportement par une classe = une table.
Comme on l'a vu dans la description des fonctionnalités, on va *grosso modo* avoir besoin des éléments suivants:
* Des listes de souhaits
* Des éléments qui composent ces listes
* Des parts pouvant composer chacun de ces éléments
* Des utilisateurs pour gérer tout ceci.
Nous proposons dans un premier temps d'éluder la gestion des utilisateurs, et de simplement se concentrer sur les fonctionnalités principales.
Cela nous donne ceci:
.. code-block:: python
# wish/models.py
from django.db import models
class Wishlist(models.Model):
pass
class Item(models.Model):
pass
class Part(models.Model):
pass
Les classes sont créées, mais vides. Entrons dans les détails.
******************
Listes de souhaits
******************
Comme déjà décrit précédemment, les listes de souhaits peuvent s'apparenter simplement à un objet ayant un nom et une description. Pour rappel, voici ce qui avait été défini dans les spécifications:
* un identifiant
* un identifiant externe
* un nom
* une description
* une date de création
* une date de modification
Notre classe ``Wishlist`` peut être définie de la manière suivante:
.. code-block:: python
# wish/models.py
class Wishlist(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
external_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
Que peut-on constater?
* Que s'il n'est pas spécifié, un identifiant ``id`` sera automatiquement généré et accessible dans le modèle. Si vous souhaitez malgré tout spécifier que ce soit un champ en particulier qui devienne la clé primaire, il suffit de l'indiquer grâce à l'attribut ``primary_key=True``.
* Que chaque type de champs (``DateTimeField``, ``CharField``, ``UUIDField``, etc.) a ses propres paramètres d'initialisation. Il est intéressant de les apprendre ou de se référer à la documentation en cas de doute.
Au niveau de notre modélisation:
* La propriété ``created_at`` est gérée automatiquement par Django grâce à l'attribut ``auto_now_add``: de cette manière, lors d'un **ajout**, une valeur par défaut ("*maintenant*") sera attribuée à cette propriété.
* La propriété ``updated_at`` est également gérée automatique, cette fois grâce à l'attribut ``auto_now`` initialisé à ``True``: lors d'une **mise à jour**, la propriété se verra automatiquement assigner la valeur du moment présent. Cela ne permet évidemment pas de gérer un historique complet et ne nous dira pas **quels champs** ont été modifiés, mais cela nous conviendra dans un premier temps.
* La propriété ``external_id`` est de type ``UUIDField``. Lorsqu'une nouvelle instance sera instanciée, cette propriété prendra la valeur générée par la fonction ``uuid.uuid4()``. *A priori*, chacun des types de champs possède une propriété ``default``, qui permet d'initialiser une valeur sur une nouvelle instance.
********
Souhaits
********
Nos souhaits ont besoin des propriétés suivantes:
* un identifiant
* l'identifiant de la liste auquel le souhait est lié
* un nom
* une description
* le propriétaire
* une date de création
* une date de modification
* une image permettant de le représenter.
* un nombre (1 par défaut)
* un prix facultatif
* un nombre de part facultatif, si un prix est fourni.
Après implémentation, cela ressemble à ceci:
.. code-block:: python
# wish/models.py
class Wish(models.Model):
wishlist = models.ForeignKey(Wishlist)
name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
picture = models.ImageField()
numbers_available = models.IntegerField(default=1)
number_of_parts = models.IntegerField(null=True)
estimated_price = models.DecimalField(max_digits=19, decimal_places=2,
null=True)
A nouveau, que peut-on constater ?
* Les clés étrangères sont gérées directement dans la déclaration du modèle. Un champ de type `ForeignKey <https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ForeignKey>`_ permet de déclarer une relation 1-N entre deux classes. Dans la même veine, une relation 1-1 sera représentée par un champ de type `OneToOneField <https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/>`_, alors qu'une relation N-N utilisera un `ManyToManyField <https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/>`_.
* L'attribut ``default`` permet de spécifier une valeur initiale, utilisée lors de la construction de l'instance. Cet attribut peut également être une fonction.
* Pour rendre un champ optionnel, il suffit de lui ajouter l'attribut ``null=True``.
* Comme cité ci-dessus, chaque champ possède des attributs spécifiques. Le champ ``DecimalField`` possède par exemple les attributs ``max_digits`` et ``decimal_places``, qui nous permettra de représenter une valeur comprise entre 0 et plus d'un milliard (avec deux chiffres décimaux).
* L'ajout d'un champ de type ``ImageField`` nécessite l'installation de ``pillow`` pour la gestion des images. Nous l'ajoutons donc à nos pré-requis, dans le fichier ``requirements/base.txt``.
*****
Parts
*****
Les parts ont besoins des propriétés suivantes:
* un identifiant
* identifiant du souhait
* identifiant de l'utilisateur si connu
* identifiant de la personne si utilisateur non connu
* un commentaire
* une date de réalisation
Elles constituent la dernière étape de notre modélisation et représente la réalisation d'un souhait. Il y aura autant de part d'un souhait que le nombre de souhait à réaliser fois le nombre de part.
Elles permettent à un utilisateur de participer au souhait émis par un autre utilisateur. Pour les modéliser, une part est liée d'un côté à un souhait, et d'autre part à un utilisateur. Cela nous donne ceci:
.. code-block:: python
from django.contrib.auth.models import User
class WishPart(models.Model):
wish = models.ForeignKey(Wish)
user = models.ForeignKey(User, null=True)
unknown_user = models.ForeignKey(UnknownUser, null=True)
comment = models.TextField(null=True, blank=True)
done_at = models.DateTimeField(auto_now_add=True)
La classe ``User`` référencée au début du snippet correspond à l'utilisateur qui sera connecté. Ceci est géré par Django. Lorsqu'une requête est effectuée et est transmise au serveur, cette information sera disponible grâce à l'objet ``request.user``, transmis à chaque fonction ou *Class-based-view*. C'est un des avantages d'un framework tout intégré: il vient *batteries-included* et beaucoup de détails ne doivent pas être pris en compte. Pour le moment, nous nous limiterons à ceci. Par la suite, nous verrons comment améliorer la gestion des profils utilisateurs, comment y ajouter des informations et comment gérer les cas particuliers.
La classe ``UnknownUser`` permet de représenter un utilisateur non enregistré sur le site et est définie au point suivant.
*********************
Utilisateurs inconnus
*********************
.. todo:: je supprimerais pour que tous les utilisateurs soient gérés au même endroit.
Pour chaque réalisation d'un souhait par quelqu'un, il est nécessaire de sauver les données suivantes, même si l'utilisateur n'est pas enregistré sur le site:
* un identifiant
* un nom
* une adresse email. Cette adresse email sera unique dans notre base de données, pour ne pas créer une nouvelle occurence si un même utilisateur participe à la réalisation de plusieurs souhaits.
Ceci nous donne après implémentation:
.. code-block:: python
class UnkownUser(models.Model):
name = models.CharField(max_length=255)
email = models.CharField(email = models.CharField(max_length=255, unique=True)

View File

@ -1,152 +0,0 @@
== Refactoring
On constate que plusieurs classes possèdent les mêmes propriétés `created_at` et `updated_at`, initialisées aux mêmes valeurs. Pour gagner en cohérence, nous allons créer une classe dans laquelle nous définirons ces deux champs, et nous ferons en sorte que les classes `Wishlist`, `Item` et `Part` en héritent. Django gère trois sortes d'héritage:
* L'héritage par classe abstraite
* L'héritage classique
* L'héritage par classe proxy.
=== Classe abstraite
L'héritage par classe abstraite consiste à déterminer une classe mère qui ne sera jamais instanciée. C'est utile pour définir des champs qui se répèteront dans plusieurs autres classes et surtout pour respecter le principe de DRY. Comme la classe mère ne sera jamais instanciée, ces champs seront en fait dupliqués physiquement, et traduits en SQL, dans chacune des classes filles.
[source,python]
----
# wish/models.py
class AbstractModel(models.Model):
class Meta:
abstract = True
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Wishlist(AbstractModel):
pass
class Item(AbstractModel):
pass
class Part(AbstractModel):
pass
----
En traduisant ceci en SQL, on aura en fait trois tables, chacune reprenant les champs `created_at` et `updated_at`, ainsi que son propre identifiant:
[source,sql]
----
--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_wishlist" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_item" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_part" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
COMMIT;
----
=== Héritage classique
L'héritage classique est généralement déconseillé, car il peut introduire très rapidement un problème de performances: en reprenant l'exemple introduit avec l'héritage par classe abstraite, et en omettant l'attribut `abstract = True`, on se retrouvera en fait avec quatre tables SQL:
* Une table `AbstractModel`, qui reprend les deux champs `created_at` et `updated_at`
* Une table `Wishlist`
* Une table `Item`
* Une table `Part`.
A nouveau, en analysant la sortie SQL de cette modélisation, on obtient ceci:
[source,sql]
----
--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_abstractmodel" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_wishlist" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_item" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_part" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
COMMIT;
----
Le problème est que les identifiants seront définis et incrémentés au niveau de la table mère. Pour obtenir les informations héritées, nous seront obligés de faire une jointure. En gros, impossible d'obtenir les données complètes pour l'une des classes de notre travail de base sans effectuer un *join* sur la classe mère.
Dans ce sens, cela va encore... Mais imaginez que vous définissiez une classe `Wishlist`, de laquelle héritent les classes `ChristmasWishlist` et `EasterWishlist`: pour obtenir la liste complètes des listes de souhaits, il vous faudra faire une jointure **externe** sur chacune des tables possibles, avant même d'avoir commencé à remplir vos données. Il est parfois nécessaire de passer par cette modélisation, mais en étant conscient des risques inhérents.
=== Classe proxy
Lorsqu'on définit une classe de type **proxy**, on fait en sorte que cette nouvelle classe ne définisse aucun nouveau champ sur la classe mère. Cela ne change dès lors rien à la traduction du modèle de données en SQL, puisque la classe mère sera traduite par une table, et la classe fille ira récupérer les mêmes informations dans la même table: elle ne fera qu'ajouter ou modifier un comportement dynamiquement, sans ajouter d'emplacements de stockage supplémentaires.
Nous pourrions ainsi définir les classes suivantes:
[source,python]
----
# wish/models.py
class Wishlist(models.Model):
name = models.CharField(max_length=255)
description = models.CharField(max_length=2000)
expiration_date = models.DateField()
@staticmethod
def create(self, name, description, expiration_date=None):
wishlist = Wishlist()
wishlist.name = name
wishlist.description = description
wishlist.expiration_date = expiration_date
wishlist.save()
return wishlist
class ChristmasWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
christmas = datetime(current_year, 12, 31)
w = Wishlist.create(name, description, christmas)
w.save()
class EasterWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
expiration_date = datetime(current_year, 4, 1)
w = Wishlist.create(name, description, expiration_date)
w.save()
----

View File

@ -1,107 +0,0 @@
== Besoins utilisateurs
Nous souhaitons développer un site où un utilisateur donné peut créer une liste contenant des souhaits et où d'autres utilisateurs, authentifiés ou non, peuvent choisir les souhaits à la réalisation desquels ils souhaitent participer.
Il sera nécessaire de s'authentifier pour :
* Créer une liste associée à l'utilisateur en cours
* Ajouter un nouvel élément à une liste
Il ne sera pas nécessaire de s'authentifier pour :
* Faire une promesse d'offre pour un élément appartenant à une liste, associée à un utilisateur.
L'utilisateur ayant créé une liste pourra envoyer un email directement depuis le site aux personnes avec qui il souhaite partager sa liste, cet email contenant un lien permettant d'accéder à cette liste.
A chaque souhait, on pourrait de manière facultative ajouter un prix. Dans ce cas, le souhait pourrait aussi être subdivisé en plusieurs parties, de manière à ce que plusieurs personnes puissent participer à sa réalisation.
Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à dupliquer le souhait en question.
== Besoins fonctionnels
=== Gestion des utilisateurs
Pour gérer les utilisateurs, nous allons faire en sorte de surcharger ce que Django propose: par défaut, on a une la possibilité de gérer des utilisateurs (identifiés par une adresse email, un nom, un prénom, ...) mais sans plus.
Ce qu'on peut souhaiter, c'est que l'utilisateur puisse s'authentifier grâce à une plateforme connue (Facebook, Twitter, Google, etc.), et qu'il puisse un minimum gérer son profil.
=== Gestion des listes
==== Modèlisation
Les données suivantes doivent être associées à une liste:
* un identifiant
* un identifiant externe (un GUID, par exemple)
* un nom
* une description
* le propriétaire, associé à l'utilisateur qui l'aura créée
* une date de création
* une date de modification
==== Fonctionnalités
* Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer une liste dont il est le propriétaire
* Un utilisateur doit pouvoir associer ou retirer des souhaits à une liste dont il est le propriétaire
* Il faut pouvoir accéder à une liste, avec un utilisateur authentifier ou non, *via* son identifiant externe
* Il faut pouvoir envoyer un email avec le lien vers la liste, contenant son identifiant externe
* L'utilisateur doit pouvoir voir toutes les listes qui lui appartiennent
=== Gestion des souhaits
==== Modélisation
Les données suivantes peuvent être associées à un souhait:
* un identifiant
* identifiant de la liste
* un nom
* une description
* le propriétaire
* une date de création
* une date de modification
* une image, afin de représenter l'objet ou l'idée
* un nombre (1 par défaut)
* un prix facultatif
* un nombre de part, facultatif également, si un prix est fourni.
==== Fonctionnalités
* Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer un souhait dont il est le propriétaire.
* On ne peut créer un souhait sans liste associée
* Il faut pouvoir fractionner un souhait uniquement si un prix est donné.
* Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié ou non.
* Il faut pouvoir réaliser un souhait ou une partie seulement, avec un utilisateur authentifié ou non.
* Un souhait en cours de réalisation et composé de différentes parts ne peut plus être modifié.
* Un souhait en cours de réalisation ou réalisé ne peut plus être supprimé.
* On peut modifier le nombre de fois qu'un souhait doit être réalisé dans la limite des réalisations déjà effectuées.
=== Gestion des réalisations de souhaits
==== Modélisation
Les données suivantes peuvent être associées à une réalisation de souhait:
* identifiant du souhait
* identifiant de l'utilisateur si connu
* identifiant de la personne si utilisateur non connu
* un commentaire
* une date de réalisation
==== Fonctionnalités
* L'utilisateur doit pouvoir voir si un souhait est réalisé, en partie ou non. Il doit également avoir un pourcentage de complétion sur la possibilité de réalisation de son souhait, entre 0% et 100%.
* L'utilisateur doit pouvoir voir la ou les personnes ayant réalisé un souhait.
* Il y a autant de réalisation que de parts de souhait réalisées ou de nombre de fois que le souhait est réalisé.
=== Gestion des personnes réalisants les souhaits et qui ne sont pas connues
==== Modélisation
Les données suivantes peuvent être associées à une personne réalisant un souhait:
* un identifiant
* un nom
* une adresse email facultative
==== Fonctionnalités

View File

@ -1,165 +0,0 @@
== Tests unitaires
=== Pourquoi s'ennuyer à écrire des tests?
Traduit grossièrement depuis un article sur `https://medium.com <https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21>`_:
Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont
Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:
1. Aide au design: écrire des tests avant d'écrire le code vous donnera une meilleure perspective sur le design à appliquer aux API.
2. Documentation (pour les développeurs): chaque description d'un test
3. Tester votre compréhension en tant que développeur:
4. Assurance qualité: des tests,
5.
=== Why Bother with Test Discipline?
Your tests are your first and best line of defense against software defects. Your tests are more important than linting & static analysis (which can only find a subclass of errors, not problems with your actual program logic). Tests are as important as the implementation itself (all that matters is that the code meets the requirementhow its implemented doesnt matter at all unless its implemented poorly).
Unit tests combine many features that make them your secret weapon to application success:
1. Design aid: Writing tests first gives you a clearer perspective on the ideal API design.
2. Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.
3. Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?
4. Quality Assurance: Manual QA is error prone. In my experience, its impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features.
5. Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.
Unit tests dont need to be twisted or manipulated to serve all of those broad-ranging goals. Rather, it is in the essential nature of a unit test to satisfy all of those needs. These benefits are all side-effects of a well-written test suite with good coverage.
=== What are you testing?
1. What component aspect are you testing?
2. What should the feature do? What specific behavior requirement are you testing?
=== Couverture de code
On a vu au chapitre 1 qu'il était possible d'obtenir une couverture de code, c'est-à-dire un pourcentage.
=== Comment tester ?
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.
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:
[source,text]
----
$ 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%:
[source,shell]
----
$ 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.
=== Quelques liens utiles
* `Django factory boy <https://github.com/rbarrois/django-factory_boy/tree/v1.0.0>`_

View File

@ -1,5 +0,0 @@
************************
Gestion des utilisateurs
************************
Dans les spécifications, nous souhaitions pouvoir associer un utilisateur à une liste (*le propriétaire*) et un utilisateur à une part (*le donateur*). Par défaut, Django offre une gestion simplifiée des utilisateurs (pas de connexion LDAP, pas de double authentification, ...): juste un utilisateur et un mot de passe. Pour y accéder, un paramètre par défaut est défini dans votre fichier de settings: ``AUTH_USER_MODEL``.

View File

@ -1,7 +0,0 @@
== Intégrations
[quote]
----
Every integration point will eventually fail in some way, and you need to be prepared for that failure.
-- Michael T. Nygard, Release It! Second Edition, Chapitre 4. Stability Antipatterns, page 45
----

View File

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

View File

@ -1,14 +0,0 @@
== Application _Legacy_
Une application _legacy_ est une application qui existait déjà avant que nous ne jetions notre dévolu sur Django.
Ce type d'application suit déjà ses propres principes, sans doute aussi ses propres conventions.
Le moteur de base de données peut être hétéroclite, les technologies
=== 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 :-)
[source,python]
----
----

View File

@ -1 +0,0 @@
= Ressources et bibliographie

View File

@ -1,11 +0,0 @@
== Applications _Legacy_
Quand on intègre une nouvelle application Django dans un environement existant, la première étape est de se câbler sur la base de données existantes;
1. Soit l'application sur laquelle on se greffe restera telle quelle;
2. Soit l'application est **remplacée** par la nouvelle application Django.
Dans le premier cas, il convient de créer une application et de spécifier pour chaque classe l'attribute `managed = False` dans le `class Meta:` de la définition.
Dans le second, il va falloir câbler deux-trois éléments avant d'avoir une intégration complète (comprendre: avec une interface d'admin, les migrations, les tests unitaires et tout le brol :))
`python manage.py inspectdb > models.py`

View File

@ -1,119 +0,0 @@
@book{clean_architecture,
title = {Clean Architecture, A Craftman's Guide to Software Structure and Design},
author = {Robert C. Martin},
publisher = {Pearson},
editor = {Addison-Wesley},
isbn = {978-0-13-449416-6},
year = {2018},
type = {Book}
}
@book{
other_side,
author = {Aurélie Jean},
title = {De l'autre côté de la machine}
}
@book{clean_code,
title = {Clean Code, a Handbook of Agile Software Craftmanship},
author = {Robert C. Martin},
publisher = {Pearson},
editor = {Addison-Wesley},
year = {2009},
type = {Book}
}
@book{expert_python,
title = {Expert Python Programming},
author = {Jaworski, Michal and Ziadé, Tarek},
publisher = {Packt Publishing},
year = {2021},
edition = {4th edition},
type = {Book}
}
@book{devops_handbook,
title = {The DevOps Handbook, How to create World-class Agility, Reliability, & Security in Technology Organizations},
author = {Gene Kim and Jez Humble and Patrick Debois and John Willis},
publisher = {IT Revolution},
year = {2016},
type = {Book}
}
@book{boring_stuff,
title = {Automate the Boring Stuff with Python},
booktitle = {Practical Programming For Total Beginners},
author = {Al Sweigart},
year = {2020},
editor = {No Starch Press},
publisher = {William Pollock},
edition = {2nd edition}
}
@book{maintainable_software,
title = {Building Maintainable Software},
booktitle = {Ten Guidelines for Future-Proof Code},
author = {Joost Visser},
year = {2016},
edition = {C# Edition, first edition},
publisher = {O'Reilly Media, Inc.},
isbn = {978-1-491-95452-2},
url = {http://shop.oreilly.com/product/0636920049555.do}
}
@book{two_scoops_of_django,
author = {Daniel Roy and Andrey Greenfeld},
title = {Two Scoops of Django},
year = {2021},
}
@book{django_design_patterns,
year = {2015},
publisher = {Packt Publishing}
}
@book{unix_philosophy,
author = {Eric S. Raymond},
year = {2004},
title = {Basics of the Unix Philosophy, The Art of Unix Programming},
publisher = {Addison-Wesley Professional},
isbn = {0-13-142901-9},
}
@book{data_intensive,
title = {Designing Data Intensive Applications},
booktitle = {The Big Ideas Behind Reliable, Scalable and Maintainable Systems},
year = {2017},
author = {Martin Kleppmann},
publisher = {O'Reilly},
isbn = {978-1-449-37332-0},
release = {Fifteenth release - 2021-03-26}
}
@misc{
django,
title = {The web framework for perfectionists with deadlines},
url = {https://www.djangoproject.com/}
}
@misc{
django_design_philosophies,
title = {Design Philosophies},
url = {https://docs.djangoproject.com/en/dev/misc/design-philosophies/}
}
@misc{
consider_sqlite,
title = {Consider SQLite},
year = {2021},
url = {https://blog.wesleyac.com/posts/consider-sqlite}
}
@misc{agiliq_admin,
title = {Django Admin Cookbook, How to do things with Django admin},
year = {2018},
url = {https://books.agiliq.com/projects/django-admin-cookbook/en/latest/}
}
@misc{agiliq_multi_tenant,
title = {Building Multi Tenant Applications with Django},
year = {2018},
url = {https://books.agiliq.com/projects/django-multi-tenant/en/latest/},
}
@misc{simple_is_better_than_complex,
author = {Vitor Freitas},
title = {A Complete Beginner's Guide to Django},
year = {2017},
note = {Last visited in 2021},
url = {https://simpleisbetterthancomplex.com/series/beginners-guide/1.11/}
}
@misc{gnu_linux_mag_hs_104,
title = {Les cinq règles pour écrire du code maintenable},
year = {2019},
url = {https://boutique.ed-diamond.com/les-hors-series/1402-gnulinux-magazine-hs-104.html}
}