diff --git a/source/deploy/admin_with_static.png b/source/deploy/admin_with_static.png deleted file mode 100755 index 66af916..0000000 Binary files a/source/deploy/admin_with_static.png and /dev/null differ diff --git a/source/deploy/admin_without_static.png b/source/deploy/admin_without_static.png deleted file mode 100755 index aa6b25c..0000000 Binary files a/source/deploy/admin_without_static.png and /dev/null differ diff --git a/source/deploy/environment.rst b/source/deploy/environment.rst deleted file mode 100755 index 8bb2df2..0000000 --- a/source/deploy/environment.rst +++ /dev/null @@ -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. diff --git a/source/deploy/gunicorn.rst b/source/deploy/gunicorn.rst deleted file mode 100755 index 9e39fb5..0000000 --- a/source/deploy/gunicorn.rst +++ /dev/null @@ -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. \ No newline at end of file diff --git a/source/deploy/nginx.rst b/source/deploy/nginx.rst deleted file mode 100755 index 6d37a45..0000000 --- a/source/deploy/nginx.rst +++ /dev/null @@ -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 `_, 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 `_ 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 `_. \ No newline at end of file diff --git a/source/deploy/update-app.adoc b/source/deploy/update-app.adoc deleted file mode 100755 index e7ba3f7..0000000 --- a/source/deploy/update-app.adoc +++ /dev/null @@ -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 ----- - -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 `_. - - * **Problème**: cette librairie n'existe que pour Python2.7. - * **Avantage**: on peut écrire du code semblable à `ceci `_... - -.. 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. diff --git a/source/glossary.adoc b/source/glossary.adoc deleted file mode 100755 index 4e1da6b..0000000 --- a/source/glossary.adoc +++ /dev/null @@ -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[] diff --git a/source/main.adoc b/source/main.adoc deleted file mode 100755 index 03e72bf..0000000 --- a/source/main.adoc +++ /dev/null @@ -1,155 +0,0 @@ -= Minor swing with Django -Cédric Declerfayt ; Fred Pauchet -: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] ----- -# /. <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::[] diff --git a/source/part-2-deployment/_main.adoc b/source/part-2-deployment/_main.adoc deleted file mode 100755 index 8d89467..0000000 --- a/source/part-2-deployment/_main.adoc +++ /dev/null @@ -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. -L’objectif est qu'il soit rapide et fiable. -Ceci peut être atteint au travers d’un partitionnement correct, incluant le fait que le composant principal s’assure 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 ! diff --git a/source/part-2-deployment/databases.adoc b/source/part-2-deployment/databases.adoc deleted file mode 100755 index e69de29..0000000 diff --git a/source/part-2-deployment/debian.adoc b/source/part-2-deployment/debian.adoc deleted file mode 100755 index 7707566..0000000 --- a/source/part-2-deployment/debian.adoc +++ /dev/null @@ -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= <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 ; - 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 - -source ~/.venvs//bin/activate -cd ~/webapps/ -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 diff --git a/source/part-2-deployment/docker.adoc b/source/part-2-deployment/docker.adoc deleted file mode 100755 index 06d9c20..0000000 --- a/source/part-2-deployment/docker.adoc +++ /dev/null @@ -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]. diff --git a/source/part-2-deployment/heroku.adoc b/source/part-2-deployment/heroku.adoc deleted file mode 100755 index f89c64f..0000000 --- a/source/part-2-deployment/heroku.adoc +++ /dev/null @@ -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/) diff --git a/source/part-2-deployment/kubernetes.adoc b/source/part-2-deployment/kubernetes.adoc deleted file mode 100755 index e69de29..0000000 diff --git a/source/part-2-deployment/logging.adoc b/source/part-2-deployment/logging.adoc deleted file mode 100755 index a73c45e..0000000 --- a/source/part-2-deployment/logging.adoc +++ /dev/null @@ -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. diff --git a/source/part-3-data-model/_index.adoc b/source/part-3-data-model/_index.adoc deleted file mode 100755 index 16f355b..0000000 --- a/source/part-3-data-model/_index.adoc +++ /dev/null @@ -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 diff --git a/source/part-3-data-model/admin.adoc b/source/part-3-data-model/admin.adoc deleted file mode 100755 index 5844db4..0000000 --- a/source/part-3-data-model/admin.adoc +++ /dev/null @@ -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 `/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] ----- - ----- diff --git a/source/part-3-data-model/auth.adoc b/source/part-3-data-model/auth.adoc deleted file mode 100755 index c1c7f80..0000000 --- a/source/part-3-data-model/auth.adoc +++ /dev/null @@ -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 `.`. - -==== 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 `_. 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 `_. - -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. diff --git a/source/part-3-data-model/context_processors.adoc b/source/part-3-data-model/context_processors.adoc deleted file mode 100755 index 7aa038d..0000000 --- a/source/part-3-data-model/context_processors.adoc +++ /dev/null @@ -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" - ], - }, - }, -] ----- diff --git a/source/part-3-data-model/forms.adoc b/source/part-3-data-model/forms.adoc deleted file mode 100755 index 2132936..0000000 --- a/source/part-3-data-model/forms.adoc +++ /dev/null @@ -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 `_, 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 ``. - * récupérer les valeurs du paramètre `choices` (si présent) pour la balise `