Merges from master.

This commit is contained in:
Fred Pauchet 2020-08-31 21:45:56 +02:00
commit bb7743459d
59 changed files with 1849 additions and 6281 deletions

193
Makefile
View File

@ -1,54 +1,10 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf xelatex text man changes linkcheck doctest coverage gettext
.PHONY: help pdf
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " xelatex to make LaTeX files and run them through xelatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " pdf to make standalone PDF files"
clean:
rm -rf $(BUILDDIR)/*
@ -57,144 +13,5 @@ html:
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Gwift.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Gwift.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Gwift"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Gwift"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
xelatex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running XeLaTeX files through xelatex..."
sed -i s/pdflatex/xelatex/ $(BUILDDIR)/latex/Makefile
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
pdf:
asciidoctor-pdf -a pdf-themesdir=resources/themes -a pdf-theme=gwift-theme source/main.adoc -t -r asciidoctor-diagram

View File

@ -39,11 +39,11 @@ $ gem install asciidoctor-diagram
```bash
asciidoctor -a rouge-style=monokai -a pdf-themesdir=resources/themes -a pdf-theme=gwift main.adoc -t -r asciidoctor-diagram
asciidoctor-pdf -a rouge-style=monokai -a pdf-themesdir=resources/themes -a pdf-theme=gwift main.adoc -t -r asciidoctor-diagram
asciidoctor-pdf -a pdf-themesdir=resources/themes -a pdf-theme=gwift main.adoc -t -r asciidoctor-diagram
```
## Configuration de l'espace utilisateur
```bash
source /usr/share/powerline/bindings/bash/powerline.sh
```
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -1,11 +0,0 @@
# Django
[Django](https://www.djangoproject.com/) est l'un des frameworks Web proposant 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.
En restant dans les sentiers battus, votre projet suivra le patron de conception `MVC` (Modèle-Vue-Controleur), avec une petite variante sur les termes utilisés: Django les nomme respectivement Modèle-Template-Vue:
* 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.
* Le template, qui s'occupe de la mise en forme: c'est le composant qui va s'occuper de transformer les données en un affichage compréhensible (avec l'aide du navigateur) pour l'utilisateur.

226
make.bat
View File

@ -1,6 +1,6 @@
@ECHO OFF
REM Command file for Sphinx documentation
REM Command file for building gwift-book
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
@ -19,25 +19,7 @@ if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
echo. pdf to make standalone PDF files
goto end
)
@ -47,31 +29,6 @@ if "%1" == "clean" (
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 2> nul
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
:sphinx_ok
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
@ -80,183 +37,8 @@ if "%1" == "html" (
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Gwift.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Gwift.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
if "%1" == "pdf" (
asciidoctor-pdf -a pdf-themesdir=resources/themes -a pdf-theme=basic source/main.adoc -t
goto end
)

View File

@ -0,0 +1,14 @@
extends: default
footer:
recto:
right:
content: '{section-or-chapter-title} | {page-number}'
verso:
left:
content: '{page-number} | {chapter-title}'
admonition:
icon:
caution:
name: fa-fire
stroke_color: ff0000
size: 24

View File

@ -1,267 +0,0 @@
== Déploiement sur CentOS
[source,bash]
----
yum update
groupadd --system webapps
groupadd --system gunicorn_sockets
useradd --system --gid webapps --shell /bin/bash --home /home/medplan medplan
mkdir -p /home/medplan
chown medplan:webapps /home/medplan
----
=== Installation des dépendances systèmes
[source,bash]
----
yum install python36 git tree -y
# CentOS 7 ne dispose que de la version 3.7 d'SQLite. On a besoin d'une version 3.8 au minimum:
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-devel-3.8.11-1.fc21.x86_64.rpm
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-3.8.11-1.fc21.x86_64.rpm
sudo yum install sqlite-3.8.11-1.fc21.x86_64.rpm sqlite-devel-3.8.11-1.fc21.x86_64.rpm -y
----
=== Préparation de l'environnement utilisateur
[source,bash]
----
su - medplan
cp /etc/skel/.bashrc .
cp /etc/skel/.bash_profile .
ssh-keygen
mkdir bin
mkdir .venvs
mkdir webapps
python3.6 -m venv .venvs/medplan
source .venvs/medplan/bin/activate
cd /home/medplan/webapps
git clone git@vmwmedtools:institutionnel/medplan.git
----
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 'medplan'
source .venvs/medplan/bin/activate
pip install -U pip
pip install -r requirements/base.txt
pip install gunicorn
cd webapps/medplan
gunicorn config.wsgi:application --bind localhost:3000 --settings=config.settings_production
----
=== Configuration de l'application
[source,bash]
----
SECRET_KEY=<set your secret key here>
ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/medplan/static
----
=== Création des répertoires de logs
[source,text]
----
mkdir -p /var/www/medplan/static
----
=== Création du répertoire pour le socket
Dans le fichier `/etc/tmpfiles.d/medplan.conf`:
[source,text]
----
D /var/run/webapps 0775 medplan gunicorn_sockets -
----
Suivi de la création par systemd :
[source,text]
----
systemd-tmpfiles --create
----
=== Gunicorn
[source,bash]
----
#!/bin/bash
# defines settings for gunicorn
NAME="Medplan"
DJANGODIR=/home/medplan/webapps/medplan
SOCKFILE=/var/run/webapps/gunicorn_medplan.sock
USER=medplan
GROUP=gunicorn_sockets
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production
DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`"
source /home/medplan/.venvs/medplan/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
[source,bash]
----
yum install supervisor -y
----
On crée ensuite le fichier `/etc/supervisord.d/medplan.ini`:
[source,bash]
----
[program:medplan]
command=/home/medplan/bin/start_gunicorn.sh
user=medplan
stdout_logfile=/var/log/medplan/medplan.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/medplan
chown medplan:nagios /var/log/medplan
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/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2317 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2318 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2321 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2322 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
└─2323 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/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
----
=== Ouverture des ports
[source,text]
----
firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload
----
=== Installation d'Nginx
[source]
----
yum install nginx -y
usermod -a -G gunicorn_sockets nginx
----
On configure ensuite le fichier `/etc/nginx/conf.d/medplan.conf`:
----
upstream medplan_app {
server unix:/var/run/webapps/gunicorn_medplan.sock fail_timeout=0;
}
server {
listen 80;
server_name <server_name>;
root /var/www/medplan;
error_log /var/log/nginx/medplan_error.log;
access_log /var/log/nginx/medplan_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/ {
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;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://medplan_app;
}
}
----
=== Configuration des sauvegardes
Les sauvegardes ont été configurées avec borg: `yum install borgbackup`.
C'est l'utilisateur medplan qui s'en occupe.
----
mkdir -p /home/medplan/borg-backups/
cd /home/medplan/borg-backups/
borg init medplan.borg -e=none
borg create medplan.borg::{now} ~/bin ~/webapps
----
Et dans le fichier crontab :
----
0 23 * * * /home/medplan/bin/backup.sh
----

View File

@ -1,85 +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 `document <https://docs.djangoproject.com/en/stable/topics/auth/>`_ 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 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 `ici <https://docs.djangoproject.com/en/1.9/topics/auth/customizing/>`_.
* 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 `des instances de type <https://docs.djangoproject.com/en/1.9/ref/contrib/auth/#user-model>`_ ``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.
.. code-block:: python
AUTH_USER_MODEL = 'myapp.MyUser'
Notez bien qu'**il ne faut pas** spécifier le package ``.models`` dans cette injection de dépendances: le schéma à indiquer est bien ``<nom de l'application>.<nom de la classe>``.
Backend
=======
[blabla...]
*********
Templates
*********
Ce qui n'existe pas par contre, ce sont les vues. Django propose donc tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors administration). En premier lieu, ces paramètres sont fixés dans le fichier `settings <https://docs.djangoproject.com/en/1.8/ref/settings/#auth>`_. On y trouve par exemple les paramètres suivants:
* ``LOGIN_REDIRECT_URL``: si vous ne spécifiez pas le paramètre ``next``, l'utilisateur sera automatiquement redirigé vers cette page.
* ``LOGIN_URL``: l'URL de connexion à utiliser. Par défaut, l'utilisateur doit se rendre sur la page ``/accounts/login``.
***********************
Social-Authentification
***********************
Voir ici : `python social auth <https://github.com/omab/python-social-auth>`_
Un petit mot sur OAuth
----------------------
OAuth
=====
OAuth est un standard libre définissant un ensemble de méthodes à implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement se base sur un système de jetons (Tokens), attribués par le possesseur de la ressource à laquelle un utilisateur souhaite accéder.
Le client initie la connexion en demandant un jeton au serveur. Ce jeton est ensuite utilisée tout au long de la connexion, pour accéder aux différentes ressources offertes par ce serveur. `wikipedia <http://en.wikipedia.org/wiki/OAuth>`_.
Une introduction à OAuth est `disponible ici <http://hueniverse.com/oauth/guide/intro/>`_. 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 `workflow <http://hueniverse.com/oauth/guide/workflow/>`_ est le suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site faji.com (A). Elle souhaite les imprimer au travers du site beppa.com (B).
Au moment de la commande, le site beppa.com envoie une demande au site faji.com pour accéder aux ressources partagées par Jane. Pour cela, une nouvelle page s'ouvre pour l'utilisateur, et lui demande d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui permet d'y accéder.

View File

@ -1,6 +0,0 @@
== Migrations
Les migrations (comprendre les "migrations du schéma de base de données") sont intimement liées à la représentation d'un contexte fonctionnel. L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre.
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. Le développeur s'occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, _squashées_, ... et feront partie intégrante du processus de mise à jour de l'application.

View File

@ -1,11 +0,0 @@
== Modèle-vue-template
Dans un *pattern* MVC classique, la traduction immédiate du **contrôleur** est une **vue**. Et comme on le verra par la suite, la **vue** est en fait le **template**.
Les vues agrègent donc les informations à partir d'un des composants et les font transiter vers un autre. En d'autres mots, la vue sert de pont entre les données gérées par la base et l'interface utilisateur.
include::mvc/views.adoc[]
include::mvc/templates.adoc[]
include::mvc/layout.adoc[]
include::mvc/urls.adoc[]
NOTE: Ne pas oublier de parler des sessions. Mais je ne sais pas si c'est le bon endroit.

View File

@ -1,55 +0,0 @@
=== URLs
La gestion des URLs permet *grosso modo* d'assigner une adresse paramétrée ou non à une fonction Python. La manière simple consiste à modifier le fichier `gwift/settings.py` pour y ajouter nos correspondances. Par défaut, le fichier ressemble à ceci:
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]
----
Le champ `urlpatterns` associe un ensemble d'adresses à des fonctions. Dans le fichier *nu*, seul le *pattern* `admin`_ est défini, et inclut toutes les adresses qui sont définies dans le fichier `admin.site.urls`. Reportez-vous à l'installation de l'environnement: ce fichier contient les informations suivantes:
.. _`admin`: Rappelez-vous de vos expressions régulières: `^` indique le début de la chaîne.
.. code-block:: python
# admin.site.urls.py
==== Reverse
En associant un nom ou un libellé à chaque URL, il est possible de récupérer sa *traduction*. Cela implique par contre de ne plus toucher à ce libellé par la suite...
Dans le fichier `urls.py`, on associe le libellé `wishlists` à l'URL `r'^$` (c'est-à-dire la racine du site):
[source,python]
----
from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]
----
De cette manière, dans nos templates, on peut à présent construire un lien vers la racine avec le tags suivant:
[source,html]
----
<a href="{% url 'wishlists' %}">{{ yearvar }} Archive</a>
----
De la même manière, on peut également récupérer l'URL de destination pour n'importe quel libellé, de la manière suivante:
[source,python]
----
from django.core.urlresolvers import reverse_lazy
wishlists_url = reverse_lazy('wishlists')
----

View File

@ -1,10 +0,0 @@
== Queryset & managers
L'ORM de Django propose par défaut deux objets hyper importants:
* Les managers, qui consistent en un point d'entrée pour accéder aux objets persistants
* Les querysets, qui permettent de filtrer des ensembles ou sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour ajouter
d'autres filtres à des filtres existants.
En plus de cela, il faut bien tenir compte des propriétés `Meta` de la classe: si elle contient déjà un ordre par défaut, celui-ci
sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

BIN
source/images/xkcd-327.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,69 +0,0 @@
.. Gwift documentation master file, created by
sphinx-quickstart on Wed Oct 28 18:48:30 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Gwift's documentation!
=================================
#####################
Deep dive into Django
#####################
.. toctree::
:maxdepth: 2
:numbered:
intro
#################
Espace de travail
#################
.. toctree::
:maxdepth: 2
:numbered:
gwift
integration
#################################
Déploiement et mise en production
#################################
.. toctree::
:maxdepth: 2
:numbered:
production
################################
Modélisation et concepts de base
################################
.. toctree::
:maxdepth: 2
:numbered:
admin
mvc
forms
auth
#########
En résumé
#########
.. toctree::
:maxdepth: 2
:numbered:
summary
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -6,6 +6,7 @@ Cédric Declerfayt <jaguarondi27@gmail.com>; Fred Pauchet <fred@grimbox.be>
:chapter-label: Chapitre
:preface-title: Préface
:source-highlighter: rouge
:icons: font
On ne va pas se 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". On se disait juste que ces tutoriaux restaient relativement haut-niveau et se limitaient à un contexte donné.
@ -13,74 +14,40 @@ L'idée du texte ci-dessous est de jeter les bases d'un bon développement, en s
Ces idées ne s'appliquent pas uniquement à Django et à son cadre de travail, ni même au langage Python. Juste que ces deux bidules sont de bons candidats et que le cadre de travail est bien défini et suffisamment flexible.
Django se présente comme un `Framework Web pour perfectionnistes ayant des deadlines <https://www.djangoproject.com/>`_.
Django se présente comme un "Framework Web pour perfectionnistes ayant des deadlines" [https://www.djangoproject.com/].
Django suit quelques principes <https://docs.djangoproject.com/en/dev/misc/design-philosophies/>:
Django suit link:quelques principes[https://docs.djangoproject.com/en/dev/misc/design-philosophies/].
* Faible couplage et forte cohésion, pour que chaque composant ait son indépendance.
* Moins de code, plus de fonctionnalités.
* `Don't repeat yourself <https://fr.wikipedia.org/wiki/Sec>`_: ne pas se répéter!
* Rapidité du développement (après une courbe d'apprentissage relativement ardue, malgré tout)
* link:Don't repeat yourself[https://fr.wikipedia.org/wiki/Sec]: ne pas se répéter!
* Rapidité du développement (après une petite courbe d'apprentissage un peu ardue au début ;-))
Mis côté à côté, l'application de ces principes permet une meilleure stabilité du projet. Dans la suite de ce chapitre, on verra comment configurer l'environnement, comment installer Django de manière isolée et comment démarrer un nouveau projet. On verra comment gérer correctement les dépendances, les versions et comment applique un score sur note code.
Mis côte à côte, l'application de ces principes permet une meilleure stabilité du projet.
Finalement, on verra aussique la configuration proposée par défaut par le framework n'est pas idéale pour la majorité des cas.
Dans la première partie, on verra comment configurer l'environnement, comment installer Django de manière isolée et comment démarrer un nouveau projet. On verra comment gérer correctement les dépendances, les versions et comment applique un score sur note code.
On verra aussique la configuration proposée par défaut par le framework n'est pas idéale pour la majorité des cas.
Pour cela, on présentera différents outils (mypy, flake8, black, ...), la rédaction de tests unitaires et d'intégration pour limiter les régressions, les règles de nomenclature et de contrôle du contenu, ainsi que les bonnes étapes à suivre pour arriver à un déploiement rapide et fonctionnel avec peu d'efforts.
Dans la seconde partie, on détaillera précisément les étapes de déploiement, avec la description et la configuration de l'infrastructure, ainsi que des exemples concrets de déploiement sur deux distributions principales (Debian et CentOS), ainsi que l'utilisation de Docker et Docker-Compose.
On abordera également la supervision et la mise à jour d'une application existante, en respectant les bonnes pratiques d'administration système.
Dans la troisième partie, on abordera les grands principes de modélisation, en suivant les lignes de conduites du cadre de travail. On abordera les concepts clés qui permettent à une application de rester maintenable, les formulaires et leurs validations, les migrations de données et l'administration.
Dans la quatrième partie, on mettra ces concepts en pratique en présentant le développement d'une "vraie" application: définition des tables, gestion des utilisateurs, ... et mise à disposition!
Et tout ça à un seul et même endroit. Oui. :-)
Bonne lecture.
= Environnement de travail
include::part-1-workspace/00-main.adoc[]
Avant de démarrer le développement, il est nécessaire de passer un peu de temps sur la configuration de l'environnement.
include::part-2-deployment/00-main.adoc[]
Les morceaux de code seront développés pour Python3.4+ et Django 1.8+. Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure.
include::part-3-django-concepts/00-main.adoc[]
**Remarque** : les commandes qui seront exécutés dans ce livre le seront depuis un shell sous GNU/Linux. Certaines devront donc être adaptées si vous êtes dans un autre environnemnet.
include::toolchain/12-factors.adoc[]
include::toolchain/venvs.adoc[]
include::toolchain/django.adoc[]
include::toolchain/maintainable-applications.adoc[]
include::toolchain/tools.adoc[]
include::toolchain/external_tools.adoc[]
include::toolchain/summary.adoc[]
= Déploiement
Et sécurisation du serveur.
include::deploy/index.adoc[]
include::deploy/centos.adoc[]
include::deploy/database.adoc[]
= Modélisation
Dans ce chapitre, on va parler de plusieurs concepts utiles au développement rapide d'une application. On parlera de modélisation, de migrations, d'administration auto-générée.
include::django/models.adoc[]
include::django/querysets.adoc[]
include::django/forms.adoc[]
include::django/migrations.adoc[]
include::django/mvc.adoc[]
include::django/logging.adoc[]
include::django/admin.adoc[]
= Go Live !
@ -114,6 +81,4 @@ include::gwift/metamodel.adoc[]
== feedbacks utilisateurs
= En bonus
include::bonus/code-snippets.adoc[]
include::part-9-bonus/00-main.adoc[]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
= Environnement de travail
Avant de démarrer le développement, il est nécessaire de passer un peu de temps sur la configuration de l'environnement.
Les morceaux de code seront développés pour Python3.6+ et Django 3.0+. Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure.
Django fonctionne sur un https://docs.djangoproject.com/en/dev/internals/release-process/[roulement de trois versions mineures pour une version majeure], clôturé par une version LTS (_Long Term Support_).
image::images/django-support-lts.png[]
Ce sera une bonne indication à prendre en considération pour nos dépendances, puisqu'en visant une version particulière, on ne devra pratiquement pas se soucier (bon, un peu quand même...) des dépendances à installer, pour peu qu'on reste sous un certain seuil.
Dans cette partie, on va parler de *méthode de travail*, avec un objectif visé. On peut éviter que l'application ne tourne que sur notre machine et que chaque déploiement ne soit une plaie à gérer. Chaque mise à jour doit se limiter à:
. démarrer un script,
. prévoir un rollback si cela plante
. se préparer une tisane en regardant nos flux RSS (si cette technologie existe encore...).
**Remarque** : les commandes qui seront exécutés dans ce livre le seront depuis un shell sous GNU/Linux. Certaines devront donc être adaptées si vous êtes dans un autre environnemnet.
include::12-factors.adoc[]
include::maintainable-applications.adoc[]
include::environment.adoc[]
include::venvs.adoc[]
include::django.adoc[]
include::unit_tests.adoc[]
include::tools.adoc[]
include::external_tools.adoc[]
include::summary.adoc[]

View File

@ -0,0 +1,42 @@
== Construire des applications maintables
Pour la méthode de travail et de développement, on va se baser sur les https://12factor.net/fr/[The Twelve-factor App] - ou plus simplement les *12 facteurs*.
L'idée derrière cette méthode consiste à pousser les concepts suivants (repris grossièrement de la https://12factor.net/fr/[page d'introduction] :
. Faciliter la mise en place de phases d'automatisation; plus simplement, faciliter les mises à jour applicatives, simplifier la gestion de l'hôte, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'https://en.wikipedia.org/wiki/Continuous_integration[intégration continue]/link:https://en.wikipedia.org/wiki/Continuous_deployment[déploiement continu]
. Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet.
. Faciliter XXX
. Augmenter l'agilité générale du projet, en permettant une meilleure évolutivité architecturale et une meilleure mise à l'échelle - _Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on n'en parle plus ;-)_.
En pratique, les idées planquées derrière les points ci-dessus permettront de monter facilement un nouvel environnement - qu'il soit sur la machine du petit nouveau dans l'équipe, sur un serveur Azure, Heroku, Digital Ocean ou votre nouveau Raspberry Pi Zéro caché à la cave.
Pour reprendre de manière très brute les différentes idées derrière cette méthode, on a:
NOTE: pensez à retravailler la partie ci-dessous; la version anglophone semble plus compréhensible... :-/
. Une base de code suivie, avec un système de contrôle de versions, signifie plusieurs déploiements
. Déclarez explicitement et isolez les dépendances
. Stockez la configuration dans lenvironnement
NOTE: quelle configuration ?
. Traitez les services externes comme des ressources attachées (*?*)
. Séparez strictement les étapes dassemblage et dexécution (*?*)
. Exécutez lapplication comme un ou plusieurs processus sans état (*?*)
. Exportez les services via des associations de ports (*?*)
. Grossissez à laide du modèle de processus (*?*)
. Maximisez la robustesse avec des démarrages rapides et des arrêts gracieux (*?*)
. Gardez le développement, la validation et la production aussi proches l'un de l'autre que possible
. Traitez les logs comme des flux dévènements (*?*)
. Lancez les processus dadministration et de maintenance comme des one-off-processes (*?*)
.Concrètement
|===
|Concept|Concept |Outil |Description
|1|Base de code suivie avec un système de contrôle de version| Git, Mercurial, SVN, ...|Chaque déploiement démarre à partir d'une base de code unique. Il n'y pas de dépôt "Prod", "Staging" ou "Dev". Il n'y en a qu'un et un seul.
|2|Déclaration explicite et isolation des dépendances| Pyenv, environnements virtuels, RVM, ...|Afin de ne pas perturber les dépendances systèmes, chaque application doit disposer d'un environnement sain par défaut.
|3|Configuration dans l'environnement| Fichiers .ENV| Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, on ne doit trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code.
|4|Services externes = ressources locales| Fichiers .ENV| Chaque ressource doit pouvoir être interchangeable avec une autre, sans modification du code source. La solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement via des variables d'environnement.
|5|Bien séparer les étapes de construction des étapes de mise à disposition| Capistrano, Gitea, un serveur d'artefacts, ...| L'idée est de pouvoir récupérer une version spécifique du code, sans que celle-ci ne puisse avoir été modifiée. Git permet bien de gérer des versions (au travers des tags), mais ces éléments peuvent sans doute être modifiés directement au travers de l'historique.
|===

View File

@ -0,0 +1,114 @@
== Django
Comme on l'a vu ci-dessus, `django-admin` permet de créer un nouveau projet. On fait ici une distinction entre un **projet** et une **application**:
* **Projet**: ensemble des applications, paramètres, pages HTML, middlwares, dépendances, etc., qui font que votre code fait ce qu'il est sensé faire.
* **Application**: *contexte* éventuellement indépendant, permettant d'effectuer une partie isolée de ce que l'on veut faire.
Pour `gwift`, on va notamment avoir
. une première application pour la gestion des listes de souhaits et des éléments,
. une deuxième application pour la gestion des utilisateurs,
. voire une troisième application qui gérera les partages entre utilisateurs et listes.
On voit bien ici le principe de **contexte**: l'application viendra avec son modèle, ses tests, ses vues et son paramétrage. Elle pourra éventuellement être réutilisée dans un autre projet. C'est en ça que consistent les https://www.djangopackages.com/[paquets Django] déjà disponibles: ce sont simplement de petites applications empaquetées pour être réutilisées (eg. https://github.com/tomchristie/django-rest-framework[Django-Rest-Framework], https://github.com/django-debug-toolbar/django-debug-toolbar[Django-Debug-Toolbar], ...).
NOTE: analyser la structure de ces paquets et comparer avec la structure finale de l'environnement.
=== Gestion
Comme expliqué un peu plus haut, le fichier `manage.py` est un *wrapper* sur les commandes `django-admin`. A partir de maintenant, nous n'utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet:
* `manage.py check` pour vérifier (en surface...) que votre projet ne rencontre aucune erreur
* `manage.py check --deploy`, pour vérifier (en surface aussi) que l'application est prête pour un déploiement.
* `manage.py runserver` pour lancer un serveur de développement
* `manage.py test` pour découvrir les tests unitaires disponibles et les lancer.
La liste complète peut être affichée avec `manage.py help`. Vous remarquerez que ces commandes sont groupées selon différentes catégories:
* **auth**: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
* **django**: vérifier la *compliance* du projet, lancer un *shell*, *dumper* les données de la base, effectuer une migration du schéma, ...
* **sessions**: suppressions des sessions en cours
* **staticfiles**: gestion des fichiers statiques et lancement du serveur de développement.
Nous verrons plus tard comment ajouter de nouvelles commandes.
=== Structure d'une application
Maintenant que l'on a vu à quoi servait `manage.py`, on peut créer notre nouvelle application grâce à la commande `manage.py startapp <label>`.
Cette application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir. Essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc `wish`. C'est parti pour `manage.py startapp wish`!
[source,bash]
----
$ python manage.py startapp wish
----
Résultat? Django nous a créé un répertoire `wish`, dans lequel on trouve les fichiers et dossiers suivants:
* `wish/admin.py` servira à structurer l'administration de notre application. Chaque information peut en effet être administrée facilement au travers d'une interface générée à la volée par le framework. On y reviendra par la suite.
* `wish/__init__.py` pour que notre répertoire `wish` soit converti en package Python.
* `wish/migrations/`, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* `wish/models.py` pour représenter et structurer nos données.
* `wish/tests.py` pour les tests unitaires.
Par soucis de clarté, déplacez ce nouveau répertoire `wish` dans votre répertoire `gwift` existant. C'est une forme de convention. La structure de vos répertoires devient celle-ci:
[source,bash]
----
(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── docs
│   └── README.md
├── gwift
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── wish <1>
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── __init__.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   └── wsgi.py
├── Makefile
├── manage.py
├── requirements
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
└── tox.ini
6 directories, 22 files
----
<1> Notre application a bien été créée, et on l'a déplacée dans le répertoire `gwift` !
* `admin.py` servira à structurer l'administration de notre application. Chaque information peut en effet être administrée facilement au travers d'une interface générée à la volée par le framework. On y reviendra par la suite.
* `__init__.py` pour que notre répertoire `wish` soit converti en package Python.
* `migrations/`, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* `models.py` pour représenter et structurer nos données.
* `tests.py` pour les tests unitaires.
=== Migrations et schéma de bases de données
https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html[reset migrations].
En gros, soit on supprime toutes les migrations (en conservant le fichier __init__.py), soit on
réinitialise proprement les migrations avec un --fake-initial (sous réserve que toutes les personnes qui
utilisent déjà le projet s'y conforment... Ce qui n'est pas gagné.
=== Tests unitaires
Plein de trucs à compléter ici ;-) Est-ce qu'on passe par pytest ou par le framework intégré ? Quels sont les avantages de l'un % à l'autre ?
* `views.py` pour définir ce que nous pouvons faire avec nos données.
NOTE: vérifier s'il s'agit bien d'une forme de convention :-p
NOTE: Vérifier aussi comment les applications sont construites. Type DRF, Django Social Auth, tout ça.

View File

@ -0,0 +1,18 @@
== Boite à outils
Un IDE :
* https://vscodium.com/[VSCodium], avec les plugins https://marketplace.visualstudio.com/items?itemName=ms-python.python[Python],
* https://www.jetbrains.com/pycharm/[PyCharm]
* https://www.vim.org/[Vim] avec les plugins https://github.com/davidhalter/jedi-vim[Jedi-Vim], https://github.com/preservim/nerdtree[nerdtree]
Un terminal :
* Si vous êtes sous Windows, https://cmder.net/[cmder].
Un gestionnaire de base de données ? PHPMyAdmin ou PgAdmin.
Un gestionnaire de mots de passe :
* https://keepassxc.org/[KeepassXC] (on en aura besoin ;-))

View File

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

View File

@ -1,6 +1,6 @@
== Construire des applications maintenables
Pour cette section, je me base d'un résumé de l'ebook **Building Maintenable Software** disponible chez `O'Reilly <http://shop.oreilly.com/product/0636920049555.do`_ qui vaut clairement le détour pour poser les bases d'un projet.
Pour cette section, je me base d'un résumé de l'ebook **Building Maintenable Software** disponible chez link:++O'Reilly++[http://shop.oreilly.com/product/0636920049555.do].
Ce livre répartit un ensemble de conseils parmi quatre niveaux de composants:
@ -34,3 +34,4 @@ Ce livre répartit un ensemble de conseils parmi quatre niveaux de composants:
Par rapport aux points repris ci-dessus, l'environnement Python et le framework Django proposent un ensemble d'outils intégrés qui permettent de répondre à chaque point. Avant d'aller plus loin, donc, un petit point sur les conventions, les tests (unitaires, orientés comportement, basés sur la documentation, ...), la gestion de version du code et sur la documentation. Plus que dans tout langage compilé, ceux-ci sont pratiquement obligatoires. Vous pourrez les voir comme une perte de temps dans un premier temps, mais nous vous promettons qu'ils vous en feront gagner par la suite.
NOTE: parlez des principes SOLID

View File

@ -0,0 +1,6 @@
== En résumé
* Créez systématiquement un environnement virtuel pour chaque projet sur lequel vous travaillez
* La description des dépendances utilisées pour un projet doivent faire partie intégrante des sources
C'est ici que le projet http://cookiecutter.readthedocs.io/en/latest/readme.html[CookieCutter] va être intéressant: les X premières étapes peuvent être *bypassées* par une simple commande.

View File

@ -0,0 +1,330 @@
== Chaîne d'outils
=== PEP8 - Style Guide for Python Code
Le langage Python fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life[Benevolent Dictator For Life].
La première qui va nous intéresser est la https://www.python.org/dev/peps/pep-0008/[PEP 8 -- Style Guide for Python Code]. Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour lindentation, le nommage des variables et des classes, … En bref, elle décrit comment écrire du code proprement pour que dautres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (`.`, le nom d'un répertoire, le nom d'un fichier `.py`, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options `--statistics -qq`.
[source,bash]
----
$ pep8 . --statistics -qq
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
----
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez `pyflakes`: il analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
Finalement, la solution qui couvre ces deux domaines existe et s'intitule https://github.com/PyCQA/flake8[flake8]. Sur base la même interface que `pep8`, vous aurez en plus tous les avantages liés à `pyflakes` concernant votre code source.
==== PEP257
NOTE: à remplir avec `pydocstyle`.
NOTE: parler de Napoleon.
==== Tests
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe `TestCase` depuis le package `django.test`:
[source,python]
----
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
----
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
==== Couverture de code
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet https://pypi.python.org/pypi/coverage/[coverage], qui se charge d'évaluer le pourcentage de code couvert par les tests. Ajoutez-le dans le fichier `requirements/base.txt`, et lancez une couverture de code grâce à la commande `coverage`. La configuration peut se faire dans un fichier `.coveragerc` que vous placerez à la racine de votre projet, et qui sera lu lors de l'exécution.
[source,bash]
----
# requirements/base.text
[...]
coverage
django_coverage_plugin
----
[source,bash]
----
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
----
NOTE: le bloc ci-dessous est à revoir pour isoler la configuration.
[source,bash]
----
$ coverage run --source "." manage.py test
$ coverage report
Name Stmts Miss Cover
---------------------------------------------
gwift\gwift\__init__.py 0 0 100%
gwift\gwift\settings.py 17 0 100%
gwift\gwift\urls.py 5 5 0%
gwift\gwift\wsgi.py 4 4 0%
gwift\manage.py 6 0 100%
gwift\wish\__init__.py 0 0 100%
gwift\wish\admin.py 1 0 100%
gwift\wish\models.py 49 16 67%
gwift\wish\tests.py 1 1 0%
gwift\wish\views.py 6 6 0%
---------------------------------------------
TOTAL 89 32 64%
----
$ coverage html
----
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. Pour gagner un peu de temps, n'hésitez pas à créer un fichier `Makefile` que vous placerez à la racine du projet. L'exemple ci-dessous permettra, grâce à la commande `make coverage`, d'arriver au même résultat que ci-dessus:
[source,makefile]
----
# Makefile for gwift
#
# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
$(error The 'coverage' command was not found. Make sure you have coverage installed)
endif
.PHONY: help coverage
help:
@echo " coverage to run coverage check of the source files."
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
----
==== Complexité de McCabe
La https://fr.wikipedia.org/wiki/Nombre_cyclomatique[complexité cyclomatique] (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:
[source,python]
----
if True == True:
pass # never happens
# continue ...
----
La condition existe, mais on ne passera jamais dedans. A l'inverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:
[source,python]
----
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
----
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusqu'à arriver à l'impression à l'écran et à la valeur de retour. La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. Si on rencontre une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour s'assurer que la couverture est complète. On devrait donc trouver:
. Un test pour entrer dans la condition `a == b`
. Un test pour entrer dans la condition `b == c`
. Un test pour entrer dans la condition `c == d`
. Un test pour entrer dans la condition `d == e`
. Et s'assurer que n'importe quel autre cas retournera la valeur `None`.
On a donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
NOTE: Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale
A nouveau, un greffon pour `flake8` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre `--max-complexity`. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
==== Documentation
Il existe plusieurs manières de générer la documentation d'un projet. Les plus connues sont http://sphinx-doc.org/[Sphinx] et http://www.mkdocs.org/[MkDocs]. Le premier a l'avantage d'être plus reconnu dans la communauté Python que l'autre, de pouvoir *parser* le code pour en extraire la documentation et de pouvoir lancer des https://duckduckgo.com/?q=documentation+driven+development&t=ffsb[tests orientés documentation]. A contrario, votre syntaxe devra respecter https://en.wikipedia.org/wiki/ReStructuredText[ReStructuredText]. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans ses résultats.
NOTE: parler aussi d'asciidoctor (même si moins bien intégré).
Dans l'immédiat, nous nous contenterons d'avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/...). Dans la continuié de `Flake8`, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.
NOTE: voir si il ne faudrait pas mieux passer par pydocstyle.
[source,bash]
----
$ pip install flake8_docstrings
----
Lancez ensuite `flake8` avec la commande `flake8 . --exclude="migrations"`. Sur notre projet (presque) vide, le résultat sera le suivant:
[source,bash]
----
$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused
----
Bref, on le voit: nous n'avons que très peu de modules, et aucun d'eux n'est commenté.
En plus de cette méthode, Django permet également de rendre la documentation accessible depuis son interface d'administration.
=== pep8, flake8, pylint
Un outil existe et listera lensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour linstaller, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (., le nom dun répertoire, le nom dun fichier .py, ...).
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez pyflakes: il analaysera vos sources à la recherche derreurs (imports inutilsés, méthodes inconnues, etc.).
Et finalement, si vous voulez grouper les deux, il existe flake8. Sur base la même interface que pep8, vous aurez en plus des avertissements concernant le code source.
[source,python]
--
from datetime import datetime
"""On stocke la date du jour dans la variable ToD4y"""
ToD4y = datetime.today()
def print_today(ToD4y):
today = ToD4y
print(ToD4y)
def GetToday():
return ToD4y
if __name__ == "__main__":
t = Get_Today()
print(t)
--
NOTE: l'exemple est sans doute un peu trop tiré par les cheveux...
L'exécution de la commande flake8 . retourne ceci:
[source,bash]
--
test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
test.py:18:1: W391 blank line at end of file
--
On trouve des erreurs:
* de *conventions*: le nombre de lignes qui séparent deux fonctions, le nombre d'espace après un opérateur, une ligne vide à la fin du fichier, ... Ces _erreurs_ n'en sont pas vraiment, elles indiquent juste de potentiels problèmes de communication si le code devait être lu ou compris par une autre personne.
* de *définition*: une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug potentiel.
L'étape d'après consiste à invoquer pylint. Lui, il est directement moins conciliant:
[source,text]
----
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10
----
En gros, j'ai programmé comme une grosse bouse anémique (et oui, le score d'évaluation du code permet bien d'aller en négatif). En vrac, on trouve des problèmes liés:
* au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)
* à des variables non définies (E0602)
* de la documentation manquante (C0114, C0116)
* de la redéfinition de variables (W0621).
Pour reprendre la http://pylint.pycqa.org/en/latest/user_guide/message-control.html[documentation], chaque code possède sa signification (ouf!):
* C convention related checks
* R refactoring related checks
* W various warnings
* E errors, for probable bugs in the code
* F fatal, if an error occurred which prevented pylint from doing further* processing.
PyLint est la version **++**, pour ceux qui veulent un code propre et sans bavure.
=== Black
By using Black, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.
Black makes code review faster by producing the smallest diffs possible. Blackened code looks the same regardless of the project youre reading. Formatting becomes transparent after a while and you can focus on the content instead.
https://black.readthedocs.io/en/stable/[Black].
Une chose qui fonctionne bien avec le langage Go, c'est que les outils de base sont intégrés au compilateur : le formatage de code et les tests unitaires sont à la portée de tout le monde au travers de deux commandes simples :
. `go fmt`
. `go test`
En Python, c'est plus complexe que cela, puisqu'il n'existe pas une manière unique d'arriver à un résultat (on l'a vu ci-dessus, rien que pour les tests, on a au moins deux librairies...).
Pour revenir à Go : est-ce que ce formatage est idéal et accepté par tout le monde ? Non.
Black fait le même travail: il arrive à un compromis entre la clarté du code, la facilité d'installation et d'intégration et un résultat. Ce résultat ne sera pas parfait, mais il conviendra dans 97,83% des cas (au moins).
=== pytest
=== mypy
=== Towncrier
voir https://pypi.org/project/towncrier/[ici]

View File

@ -0,0 +1,91 @@
== Tests unitaires
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, on devrait arriver à quelque chose de synthéthique:
* Au niveau de l'infrastructure,
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. la requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. elle est ensuite transmise par socket et est prise en compte par Gunicorn
. qui la transmet ensuite à l'un de ses _workers_ (= un processus Python)
. après exécution, une réponse est renvoyée à l'utilisateur.
image::images/diagrams/architecture.png[]
* Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
. et renvoyer une réponse à l'utilisateur.
image::images/diagrams/django-process.png[]
En gros, ça peut planter aux points suivants :
. L'utilisateur n'est pas connecté à l'Internet
. La version du navigateur utilisé est obsolète et les technologies de sécurité ne sont plus supportées
. La version d'HTTP n'est pas prise en charge par notre serveur
. Le verbe HTTP n'est pas pris en charge par la fonction
. Le système n'a plus de place sur son disque
. Nginx est mal configuré
. La communication par socket est mal configurée
. L'URL est inconnue
. La route n'existe pas
. La base de dnnées est inaccessible
. La fonction n'est pas correctement appelée
. Il manque des valeurs au rendu
. Il y a tellement de données qu'Nginx ou Gunicorn ou déjà envoyé une fin de requête à l'utilisateur (_timeout_)
...
En bref, on a potentiellement un ou plusieurs problèmes potentiels à chaque intervenant. Une chose à la fois: dans un premier temps, on va se concentrer sur notre code.
Vous aurez remarqué ci-dessus qu'une nouvelle application créée par Django vient d'office avec un fichier `tests.py`. C'est simplement parce que les tests unitaires et tests d'intégration font partie intégrante du cadre de travail. Ceci dit, chaque batterie de tests demande une certaine préparation et un certain "temps de cuisson"... Comprendre qu'un test ne sera effectif que s'il est correctement structurée (_configuration over convention_ ?), s'il se trouve dans une classe et que chaque test se trouve bien dans une méthode de cette classe... Cela commence à faire beaucoup de boulot pour vérifier qu'une fonction retourne bien une certaine valeur, et en décourager la plupart d'entrée de jeux, surtout quand on sait que chaque fonction, condition ou point d'entrée sera sensée disposer de son test.
Au niveau des améliorations, on va :
. Changer de framework de test et utiliser https://docs.pytest.org/en/latest/[pytest] et son greffon https://pytest-django.readthedocs.io/en/latest/[pytest-django]
. Ajouter une couverture de code.
=== Pytest
Pourquoi pytest, plutôt que Django ? Par pure fainéantise :-) Si, si ! Pytest est relativement plus facile à utiliser, permet un contrôle plus fin de certaines variables d'environnement et est surtout compatible hors-Django (en plus de quelques améliorations sur les performances, comme les tests sur plusieurs processus).
Cela signifie surtout que si vous apprenez Pytest maintenant, et que votre prochain projet est une application en CLI ou avec un autre framework, vous ne serez pas dépaysé. Les tests unitaires de Django sont compatibles uniquement avec Django... D'où une certaine perte de vélocité lorsqu'on devra s'en détacher.
Vous trouverez ci-dessous une comparaison entre des tests avec les deux frameworks:
[source,python]
----
# avec django
----
[source,python]
----
# avec pytest
----
Plugins pytest :
* https://github.com/CFMTech/pytest-monitor
* https://pivotfinland.com/pytest-sugar/[pytest-sugar]
=== Fixtures
https://realpython.com/django-pytest-fixtures/[Lien]: super bien expliqué, et pourquoi les fixtures dans Pytest c'est 'achement plus mieux que les tests unitaires de Django.
=== Couverture de code
Dans un premier temps, *le pourcentage de code couvert par nos tests*. Une fois ce pourcentage évalué, le but du jeu va consister à *ce que ce pourcentage reste stable ou augmente*. Si vous modifiez une ligne de code et que la couverture passe de 73% à 72%, vous avez perdu et vous devez faire en sorte de corriger.
Plein de trucs à compléter ici ;-) Est-ce qu'on passe par pytest ou par le framework intégré ? Quels sont les avantages de l'un % à l'autre ?
* `views.py` pour définir ce que nous pouvons faire avec nos données.

View File

@ -0,0 +1,211 @@
== Travailler en isolation
On va aborder la gestion et l'isolation des dépendances. Il est tout à fait possible de s'en passer complètement dans le cadre de "petits" projets ou d'applications déployées sur des machines dédiées, et de fonctionner à grand renforts de "sudo" et d'installation globale des dépendances.
Cette section est aussi utile pour une personne travaillant seule, que pour transmettre les connaissances à un nouveau membre de l'équipe ou pour déployer l'application elle-même.
Cette pratique est cependant fortement déconseillée pour plusieurs raisons:
. Il est tout à fait envisagable que deux applications différentes soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance.
. Pour la reproductibilité d'un environnement spécifique. Cela évite notamment les réponses type "Ca juste marche chez moi", puisque la construction d'un nouvel environnement fait partie intégrante du processus de construction et de la documentation du projet; grace à elle, on a la possibilité de construire un environnement sain et d'appliquer des dépendances identiques, quelle que soit la machine hôte.
image::images/it-works-on-my-machine.jpg
Depuis la version 3.5 de Python, le module `venv` est https://docs.python.org/3/library/venv.html[recommandé] afin de créer un environnement virtuel.
Il existe plusieurs autres modules permettant d'arriver au même résultat, avec quelques avantages et inconvénients pour chacun d'entre eux.
NOTE: parler ici de poetry, pip, pipenv et rebondir sur le point 2 des 12 facteurs.
=== Création de l'environnement virtuel
Commencons par créer un environnement virtuel, afin d'y stocker les dépendances. Placez-vous dans le répertoire dans lequel vous pourrez stocker tous vos environnements (ces environnements sont indépendants des sources; ils peuvent donc être placés n'importe où sur votre disque - évitez peut-être juste de les mettre pile dans le même répertoire que votre code source). Lancez ensuite la commande `python3 -m venv gwift-env`.
Ceci créera l'arborescence de fichiers suivante, qui peut à nouveau être un peu différente en fonction du système d'exploitation:
[source,bash]
----
fred@aerys:~/Sources/.venvs/gwift-env$ ls
bin include lib lib64 pyvenv.cfg share
----
Nous pouvons ensuite l'activer grâce à la commande `source gwift-env/bin/activate`.
[source,bash]
----
(gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$ <1>
----
<1> Le *shell* signale que nous sommes bien dans l'environnement `gwift-env`.
Par la suite, nous considérerons que l'environnement virtuel est toujours activé, même si `gwift-env` à chaque snippet de code.
A présent que l'environnement est activé, tous les binaires de cet environnement prendront le pas sur les binaires du système. De la même manière, une variable `PATH` propre est définie et utilisée, afin que les librairies Python y soient stockées. C'est donc dans cet environnement virtuel que nous retrouverons le code source de Django, ainsi que des librairies externes pour Python une fois que nous les aurons installées.
Pour gérer des versions différentes d'une même librairie, il nous suffit de jongler avec autant d'environnements que nécessaires. Une application nécessite une version de Django inférieure à la 2.0 ? On crée un environnement, on l'active et on installe ce qu'il faut.
Cette technique fonctionnera autant pour un poste de développement que sur les serveurs destinés à recevoir notre application.
Pour désactiver l'environnement virtuel, il suffit d'utiliser la commande `deactivate`
=== Installation de Django et création du répertoire de travail
Après avoir activé l'environnement, on peut à présent y installer Django. Comme expliqué ci-dessus, la librairie restera indépendante du reste du système, et ne polluera aucun autre projet.
C'est parti: `pip install 'django<3.1' !
[source,bash]
----
$ pip install django
Collecting django
Downloading Django-3.0.5
100% |################################|
Installing collected packages: django
Successfully installed django-3.0.5
----
Les commandes de création d'un nouveau site sont à présent disponibles, la principale étant `django-admin startproject`. Par la suite, nous utiliserons `manage.py`, qui constitue un *wrapper* autour de `django-admin`.
Pour démarrer notre projet, nous lançons donc `django-admin startproject gwift`.
[source,bash]
----
$ django-admin startproject gwift
----
Cette action a pour effet de créer un nouveau dossier `gwift`, dans lequel on trouve la structure suivante:
[source,bash]
----
$ tree gwift
gwift
├── gwift
| |── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
----
C'est sans ce répertoire que vont vivre tous les fichiers liés au projet. Le but est de faire en sorte que toutes les opérations (maintenance, déploiement, écriture, tests, ...) puissent se faire à partir d'un seul point d'entrée.
L'utilité de ces fichiers est définie ci-dessous:
* `settings.py` contient tous les paramètres globaux à notre projet.
* `urls.py` contient les variables de routes, les adresses utilisées et les fonctions vers lesquelles elles pointent.
* `manage.py`, pour toutes les commandes de gestion.
* `asgi.py` contient la définition de l'interface https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface[ASGI], le protocole pour la passerelle asynchrone entre votre application et le serveur Web.
* `wsgi.py` contient la définition de l'interface https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface[WSGI], qui permettra à votre serveur Web (Nginx, Apache, ...) de faire un pont vers votre projet.
NOTE: déplacer la configuration dans un répertoire `config` à part.
Tant qu'on y est, nous pouvons rajouter les répertoires utiles à la gestion de notre projet, à savoir la documentation, les dépendances et le README:
[source,bash]
----
$ mkdir docs requirements
$ touch docs/README.md
----
[source,bash]
----
(gwift) fred@aerys:~/Sources$ tree gwift
gwift
├── docs
│   └── README.md
├── gwift
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
----
=== Gestion des dépendances
Comme nous venons d'ajouter une dépendance à notre projet, profitons-en pour créer un fichier reprenant tous les dépendances de notre projet. Celles-ci sont normalement placées dans un fichier `requirements.txt`. Dans un premier temps, ce fichier peut être placé directement à la racine du projet, mais on préférera rapidement le déplacer dans un sous-répertoire spécifique (`requirements`), afin de grouper les dépendances en fonction de leur utilité:
* `base.txt`
* `dev.txt`
* `production.txt`
Au début de chaque fichier, il suffit d'ajouter la ligne `-r base.txt`, puis de lancer l'installation grâce à un `pip install -r <nom du fichier>`. De cette manière, il est tout à fait acceptable de n'installer `flake8` et `django-debug-toolbar` qu'en développement par exemple. Dans l'immédiat, on va simplement ajouter `django` dans une version strictement inférieure à la version 3.1 dans le fichier `requirements/base.txt`.
[source,bash]
----
$ echo 'django<3.1' > requirements/base.txt
$ echo '-r base.txt' > requirements/prod.txt
$ echo '-r base.txt' > requirements/dev.txt
----
Prenez directement l'habitude de spécifier la version ou les versions compatibles: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets. Des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc.
Pour être sûr et certain le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances.
Avec les mécanismes d'intégration continue et de tests unitaires, on verra plus loin comment se prémunir d'un changement inattendu.
=== Matrice de compatibilité
Décrire un fichier tox.ini
[source,bash]
----
$ touch tox.ini
----
=== Licence
Décrire une licence ? :-)
[source,bash]
----
$ touch LICENCE
----
=== Configuration globale
Décrire le fichier setup.cfg
[source,bash]
----
$ touch setup.cfg
----
=== Makefile
Décrire le makefile :)
[source,bash]
----
$ touch Makefile
----
=== Structure finale de l'environnement
Nous avons donc la strucutre finale pour notre environnement de travail:
[source,bash]
----
$ (gwift) fred@aerys:~/Sources/gwift$ tree gwift
gwift
├── docs
│   └── README.md
├── gwift
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── Makefile
├── manage.py
├── requirements
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
└── tox.ini
3 directories, 13 files
----

View File

@ -1,6 +1,6 @@
== Un peu de théorie... Les principales étapes
= Déploiement
On va déjà parler de déploiement. Le serveur que django met à notre disposition est prévu uniquement pour le développement: inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...). De même, la base de donnée doit supporter plus qu'un seul utilisateur: SQLite fonctionne très bien dès lors qu'on se limite à un seul utilisateur... Sur une application Web, il est plus que probable que vous rencontriez rapidement des erreurs de base de données verrouillée pour écriture par un autre processus. Il est donc plus que bénéfique de passer sur quelque chose de plus solide.
On va déjà parler de déploiement. Le serveur que django met à notre disposition est prévu uniquement pour le développement: inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...). De même, la base de donnée doit supporter plus qu'un seul utilisateur: SQLite fonctionne très bien dès lors qu'on se limite à un seul utilisateur... Sur une application Web, il est plus que probable que vous rencontriez rapidement des erreurs de base de données verrouillée pour écriture par un autre processus. Il est donc plus que bénéfique de passer sur quelque chose de plus solide. https://docs.djangoproject.com/fr/3.0/howto/deployment/[Déploiement].
Si vous avez suivi les étapes jusqu'ici, vous devriez à peine disposer d'un espace de travail proprement configuré, d'un modèle relativement basique et d'une configuration avec une base de données simpliste. En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce que vous souhaitez au final.
@ -15,16 +15,41 @@ Dans cette partie, on abordera les points suivants:
* Les différentes méthodes de supervision de l'application: comment analyser les fichiers de logs et comment intercepter correctement une erreur si elle se présente et comment remonter l'information.
* Une partie sur la sécurité et la sécurisation de l'hôte.
=== Définition de l'infrastructure
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, on devrait arriver à quelque chose de synthéthique:
* Au niveau de l'infrastructure,
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. la requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. elle est ensuite transmise par socket et est prise en compte par Gunicorn
. qui la transmet ensuite à l'un de ses _workers_ (= un processus Python)
. après exécution, une réponse est renvoyée à l'utilisateur.
image::images/diagrams/architecture.png[]
* Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
. et renvoyer une réponse à l'utilisateur.
image::images/diagrams/django-process.png[]
== Définition de l'infrastructure
Comme on l'a 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. On peut ainsi commencer petit, et suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, ajouter un mécanisme de mise en cache, des logiciels de suivi, ...
Pour une mise ne production, le standard *de facto* est le suivant:
* Nginx comme serveur principal
* Gunicorn comme serveur d'application
* Nginx comme reverse proxy
* Gunicorn ou Uvicorn comme serveur d'application
* Supervisorctl pour le monitoring
* PostgreSQL comme base de données.
* PostgreSQL ou MariaDB comme base de données.
* Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).
En mode _containers_, on passera plutôt par Docker et Traefik.
C'est celle-ci que nous allons décrire ci-dessous.
@ -39,13 +64,43 @@ entity "Gunicorn (sockets/HTTP)" as gunicorn
database PGSQL
--
Aussi : Docker, Heroku, Digital Ocean, Scaleway, OVH, ... Bref, sur Debian et CentOS pour avoir un panel assez large. On oublie Windows.
Aussi : Docker, Heroku, Digital Ocean, Scaleway, OVH, Ansible, Puppet, Chef, ... Bref, sur Debian et CentOS pour avoir un panel assez large. On oublie Windows: rien que Gunicorn et Nginx n'y tournent pas.
=== Mise à jour
Script de mise à jour.
[source,bash]
----
su - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
gunicorn reload -HUP
----
WARNING: le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt source.
=== Supervision
Qu'est-ce qu'on fait des logs après ? :-)
. Sentry
. Nagios
. LibreNMS
. Zabbix
include::infrastructure.adoc[]
include::database.adoc[]
include::centos+debian.adoc[]
== Ressources
https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/

View File

@ -0,0 +1,319 @@
== Déploiement sur CentOS
[source,bash]
----
yum 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 pourrait sans doute fusioner les étapes 3, 4 et 5.
=== 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
# CentOS 7 ne dispose que de la version 3.7 d'SQLite. On a besoin d'une version 3.8 au minimum:
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-devel-3.8.11-1.fc21.x86_64.rpm
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-3.8.11-1.fc21.x86_64.rpm
sudo yum install sqlite-3.8.11-1.fc21.x86_64.rpm sqlite-devel-3.8.11-1.fc21.x86_64.rpm -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.
=== Préparation de l'environnement utilisateur
[source,bash]
----
su - gwift
cp /etc/skel/.bashrc .
cp /etc/skel/.bash_profile .
ssh-keygen
mkdir bin
mkdir .venvs
mkdir webapps
python3.6 -m venv .venvs/gwift
source .venvs/gwift/bin/activate
cd /home/gwift/webapps
git clone ...
----
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes suivantes:
[source,bash]
----
# en tant qu'utilisateur 'gwift'
source .venvs/gwift/bin/activate
pip install -U pip
pip install -r requirements/base.txt
pip install gunicorn
cd webapps/gwift
gunicorn config.wsgi:application --bind localhost:3000 --settings=config.settings_production
----
=== Configuration de l'application
[source,bash]
----
SECRET_KEY=<set your secret key here> <1>
ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/gwift/static
DATABASE= <2>
----
<1> La variable `SECRET_KEY` est notamment utilisée pour le chiffrement des sessions.
<2> On fait confiance à django_environ pour traduire la chaîne de connexion à la base de données.
=== Création des répertoires de logs
[source,text]
----
mkdir -p /var/www/gwift/static
----
=== Création du répertoire pour le socket
Dans le fichier `/etc/tmpfiles.d/gwift.conf`:
[source,text]
----
D /var/run/webapps 0775 gwift gunicorn_sockets -
----
Suivi de la création par systemd :
[source,text]
----
systemd-tmpfiles --create
----
=== Gunicorn
[source,bash]
----
#!/bin/bash
# defines settings for gunicorn
NAME="gwift"
DJANGODIR=/home/gwift/webapps/gwift
SOCKFILE=/var/run/webapps/gunicorn_gwift.sock
USER=gwift
GROUP=gunicorn_sockets
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production
DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`"
source /home/gwift/.venvs/gwift/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user $USER \
--bind=unix:$SOCKFILE \
--log-level=debug \
--log-file=-
----
=== Supervision, keep-alive et autoreload
Pour la supervision, on passe par Supervisor. Il existe d'autres superviseurs,
[source,bash]
----
yum install supervisor -y
----
On crée ensuite le fichier `/etc/supervisord.d/gwift.ini`:
[source,bash]
----
[program:gwift]
command=/home/gwift/bin/start_gunicorn.sh
user=gwift
stdout_logfile=/var/log/gwift/gwift.log
autostart=true
autorestart=unexpected
redirect_stdout=true
redirect_stderr=true
----
Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu'il tourne correctement:
[source,bash]
----
mkdir /var/log/gwift
chown gwift:nagios /var/log/gwift
systemctl enable supervisord
systemctl start supervisord.service
systemctl status supervisord.service
● supervisord.service - Process Monitoring and Control Daemon
Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago
Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code=exited, status=0/SUCCESS)
Main PID: 2310 (supervisord)
CGroup: /system.slice/supervisord.service
├─2310 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf
├─2313 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2317 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2318 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2321 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2322 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
└─2323 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
ls /var/run/webapps/
----
On peut aussi vérifier que l'application est en train de tourner, à l'aide de la commande `supervisorctl`:
[source,bash]
----
$$$ supervisorctl status gwift
gwift RUNNING pid 31983, uptime 0:01:00
----
Et pour gérer le démarrage ou l'arrêt, on peut passer par les commandes suivantes:
[source,bash]
----
$$$ supervisorctl stop gwift
gwift: stopped
root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift
gwift: started
root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift
gwift: stopped
gwift: started
----
=== Ouverture des ports
et 443 (HTTPS).
[source,text]
----
firewall-cmd --permanent --zone=public --add-service=http <1>
firewall-cmd --permanent --zone=public --add-service=https <2>
firewall-cmd --reload
----
<1> On ouvre le port 80, uniquement pour autoriser une connexion HTTP, mais qui sera immédiatement redirigée vers HTTPS
<2> Et le port 443 (forcément).
=== Installation d'Nginx
[source]
----
yum install nginx -y
usermod -a -G gunicorn_sockets nginx
----
On configure ensuite le fichier `/etc/nginx/conf.d/gwift.conf`:
----
upstream gwift_app {
server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0;
}
server {
listen 80;
server_name <server_name>;
root /var/www/gwift;
error_log /var/log/nginx/gwift_error.log;
access_log /var/log/nginx/gwift_access.log;
client_max_body_size 4G;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /static/ { <2>
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; <3>
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_app;
}
}
----
<2> 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.
<3> Afin d'éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1
=== 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.

View File

@ -1,14 +1,18 @@
On l'a déjà vu, Django se base sur un pattern type ActiveRecords pour l'ORM.
== Bases de données
NOTE: à vérifier ;-)
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:
et supporte les principaux moteurs de bases de données connus: MariaDB (en natif depuis Django 3.0), PostgreSQL au travers de psycopg2 (en natif aussi), Microsoft SQLServer grâce aux drivers [...à compléter] ou Oracle via https://oracle.github.io/python-cx_Oracle/[cx_Oracle].
* 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].
WARNING: 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. De 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).
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
=== PostgreSQL
On commence par installer PostgreSQL.
@ -46,7 +50,14 @@ $$$
NOTE: penser à inclure un bidule pour les backups.
== MariaDB
=== 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

View File

@ -0,0 +1 @@
See https://mattsegal.dev/nginx-django-reverse-proxy-config.html

View File

@ -0,0 +1,39 @@
= Django
Dans ce chapitre, on va parler de plusieurs concepts utiles au développement rapide d'une application. On parlera de modélisation, de migrations, d'administration auto-générée. C'est un framework Web proposant 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.
En restant dans les sentiers battus, votre projet suivra le patron de conception `MVC` (Modèle-Vue-Controleur), avec une petite variante sur les termes utilisés: Django les nomme respectivement Modèle-Template-Vue:
Dans un *pattern* MVC classique, la traduction immédiate du **contrôleur** est une **vue**. Et comme on le verra par la suite, la **vue** est en fait le **template**.
* 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 va s'occuper 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, on a une requête qui est émise par un utilisateur. La première étape consiste à trouver une route qui correspond à cette requête, c'est à dire à trouver la correspondance entre l'URL demandée et la fonction qui sera exécutée. Cette fonction correspond au *contrôleur* et s'occupera de construire le *modèle* correspondant.
En simplifiant, Django suit bien le modèle MVC, et toutes ces étapes sont liées ensemble grâce aux différentes routes, définies dans les fichiers `urls.py`.
include::models.adoc[]
include::admin.adoc[]
include::forms.adoc[]
include::views.adoc[]
include::templates.adoc[]
include::layout.adoc[]
include::urls.adoc[]
include::auth.adoc[]
include::logging.adoc[]
NOTE: Ne pas oublier de parler des sessions. Mais je ne sais pas si c'est le bon endroit.
include::multilingual.adoc[]

View File

@ -1,16 +1,16 @@
== 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.
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.
C'est faux.
L'administration est une sorte de tour de contrôle évoluée; 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].
L'administration est une sorte de tour de contrôle évoluée; 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.
Elle 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 un excellent outil de prototypage et de preuve de concept.
Une bonne idée consiste à développer l'administration dans un premier temps, en *gardant en tête qu'il sera nécessaire de développer des concepts spécifiques*. Dans cet objectif, l'administration est un outil exceptionel, qui permet de valider un modèle, de créer des objets rapidement et de valider les liens qui existent entre eux.
C'est un excellent outil de prototypage et de preuve de concept.
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):

View File

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

View File

@ -2,13 +2,13 @@
Ou comment valider proprement des données entrantes.
NOTE: intégrer le dessin XKCD avec Little Bobby Table sur l'assainissement des données en entrée :-p
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.
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.
@ -19,13 +19,22 @@ Le flux à suivre est le suivant:
. Traitement, si la validation a réussi.
Ils jouent également plusieurs rôles:
Ils jouent également deux rôles importants:
. Validation des données, en plus de celles déjà définies au niveau du modèle
. Contrôle sur le rendu à appliquer aux champs
. 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.
@ -51,10 +60,13 @@ Le formulaire permet également de contrôler le rendu qui sera appliqué lors d
[source,python]
----
from django import forms
from datetime import date
from django import forms
from .models import Accident
class AccidentForm(forms.ModelForm):
class Meta:
model = Accident
@ -72,17 +84,18 @@ class AccidentForm(forms.ModelForm):
'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.
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 `Django-Crispy-Forms <http://django-crispy-forms.readthedocs.io/en/latest/>`_. Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le *layout* et la présentation.
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)
@ -96,11 +109,8 @@ Pour chaque champ, crispy-forms va :
http://dotmobo.github.io/django-crispy-forms.html
== Validation des données
NOTE: parler ici des méthodes `clean`.
== En conclusion
=== En conclusion
. Toute donnée entrée par l'utilisateur **doit** passer par une instance de `form`.
. euh ?

View File

@ -0,0 +1,66 @@
== Modélisation
On va aborder la modélisation des objets en elle-même, qui s'apparente à la conception de la base de données.
Django utilise un modèle https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[ORM] - c'est-à-dire que chaque objet peut s'apparenter à une table SQL, mais en ajoutant une couche propre au paradigme orienté objet. Il sera ainsi possible de définir facilement des notions d'héritage (tout en restant dans une forme d'héritage simple), la possibilité d'utiliser des propriétés spécifiques, des classes intermédiaires, ...
L'avantage de tout ceci est que tout reste au niveau du code. Si l'on revient sur la méthodologie des douze facteurs, ce point concerne principalement la minimisation de la divergence entre les environnements d'exécution. Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande, dans la mesure où *tout est embarqué au niveau du code*.
Assez de blabla, on démarre !
=== Types de champs
=== Clés étrangères et relations
=== Querysets et managers
L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants:
* Les managers, qui consistent en un point d'entrée pour accéder aux objets persistants
* Les querysets, qui permettent de filtrer des ensembles ou sous-ensemble d'objets. Les querysets peuvent s'imbriquer, pour ajouter
d'autres filtres à des filtres existants.
Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut `objects`, qui correspond à un manager (ou un gestionnaire, si vous préférez). Ce gestionnaire constitue l'interface par laquelle vous accéderez à la base de données. Mais pour cela, vous aurez aussi besoin d'appliquer certains requêtes ou filtres. Et pour cela, vous aurez besoin des `querysets`, qui consistent en des ... ensembles de requêtes :-).
[source,python]
----
from core.models import Wish
Wish.objects <1>
Wish.objects.filter(name__icontains="test").filter(name__icontains="too") <2>
----
<1> Ca, c'est notre manager.
<2> Et là, on chaîne les requêtes pour composer une recherche sur tous les souhaits dont le nom contient (avec une casse insensible) la chaîne "test" et dont le nom contient la chaîne "too".
=== Propriétés Meta
En plus de cela, il faut bien tenir compte des propriétés `Meta` de la classe: si elle contient déjà un ordre par défaut, celui-ci sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe.
[source,python]
----
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',) <1>
----
<1> On définit un ordre par défaut, directement au niveau du modèle. Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre (la méthode `order_by` existe et peut être chaînée à n'importe quel queryset). D'où l'intérêt de tester ce type de comportement, dans la mesure où un `top 1` dans votre code pourrait être modifié simplement par cette petite information.
Les propriétés de la classe Meta les plus utiles sont les suivates:
* `ordering` pour spécifier un ordre de récupération spécifique.
* `verbose_name` pour indiquer le nom à utiliser au singulier pour définir votre classe
* `verbose_name_plural`, pour le pluriel.
=== Migrations
Les migrations (comprendre les "migrations du schéma de base de données") sont intimement liées à la représentation d'un contexte fonctionnel. L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre.
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. Le développeur s'occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, _squashées_, ... et feront partie intégrante du processus de mise à jour de l'application.
=== Shell

View File

@ -0,0 +1,4 @@
Templates tags
--------------
https://www.djangotemplatetagsandfilters.com/[Django Templates and Filters]

View File

@ -1,4 +1,4 @@
=== Templates
== Templates
Avant de commencer à interagir avec nos données au travers de listes, formulaires et IHM sophistiquées, quelques mots sur les templates: il s'agit en fait de *squelettes* de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher dans le format que vous définirez. En intégrant un ensemble de *tags*, cela vous permettra de greffer les données reçues en entrée dans un patron prédéfini.
@ -43,9 +43,54 @@ En reprenant l'exemple de la page HTML définie ci-dessus, on pourra l'agrément
</html>
----
Vous pouvez déjà copier ce contenu dans un fichier `templates/wsh/list.html`, on en aura besoin par la suite.
image::images/html/my-first-wishlists.png[]
==== Structure et configuration
Mais plutôt que de réécrire à chaque fois le même entête, on peut se simplifier la vie en implémentant un héritage au niveau des templates. Pour cela, il suffit de définir des blocs de contenu, et d'*étendre* une page de base, puis de surcharger ces mêmes blocs.
Par exemple, si on repart de notre page de base ci-dessus, on va y définir deux blocs réutilisables:
[source,html]
----
<!-- templates/base.html -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>{% block title %}Gwift{% endblock %}</title> <1>
</head>
<body>
{% block body %}<p>Hello world!</p>{% endblock %} <2>
</body>
</html>
----
<1> Un bloc `title`
<2> Un bloc `body`
La page HTML pour nos listes de souhaits devient alors:
[source,html]
----
<!-- templates/wishlist/wishlist_list.html -->
{% extends "base.html" %} <1>
{% block title %}{{ block.super }} - Listes de souhaits{% endblock %} <2>
{% block body %} <3>
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
----
<1> On étend/hérite de notre page `base.html`
<2> On redéfinit le titre (mais on réutilise le titre initial en appelant `block.super`)
<3> On définit uniquement le contenu, qui sera placé dans le bloc `body`.
=== Structure et configuration
Il est conseillé que les templates respectent la structure de vos différentes applications, mais dans un répertoire à part. Par convention, nous les placerons dans un répertoire `templates`. La hiérarchie des fichiers devient alors celle-ci:
@ -70,37 +115,63 @@ TEMPLATES = [
]
----
==== Builtins
=== Builtins
Django vient avec un ensemble de *tags*. On a vu la boucle `for` ci-dessus, mais il existe https://docs.djangoproject.com/fr/1.9/ref/templates/builtins/[beaucoup d'autres tags nativement présents]. Les principaux sont par exemple:
* `{% if ... %} ... {% elif ... %} ... {% else %} ... {% endif %}`: permet de vérifier une condition et de n'afficher le contenu du bloc que si la condition est vérifiée.
* Opérateurs de comparaisons: `<`, `>`, `==`, `in`, `not in`.
* Regroupements avec le tag `{% regroup ... by ... as ... %}`.
* `{% url %}` pour construire facilement une URL
* `{% url %}` pour construire facilement une URL à partir de son nom
* `urlize` qui permet de remplacer des URLs trouvées dans un champ de type CharField ou TextField par un lien cliquable.
* ...
==== Non-builtins
Chacune de ces fonctions peut être utilisée autant au niveau des templates qu'au niveau du code. Il suffit d'aller les chercher dans le package `django.template.defaultfilters`. Par exemple:
[source,python]
----
from django.db import models
from django.template.defaultfilters import urlize
class Suggestion(moels.Model):
"""Représentation des suggestions.
"""
subject = models.TextField(verbose_name="Sujet")
def urlized_subject(self):
"""
Voir https://docs.djangoproject.com/fr/3.0/howto/custom-template-tags/
"""
return urlize(self.subject, autoescape=True)
----
=== Non-builtins
En plus des quelques tags survolés ci-dessus, il est également possible de construire ses propres tags. La structure est un peu bizarre, car elle consiste à ajouter un paquet dans une de vos applications, à y définir un nouveau module et à y définir un ensemble de fonctions. Chacune de ces fonctions correspondra à un tag appelable depuis vos templates.
Il existe trois types de tags *non-builtins*:
1. Les filtres - on peut les appeler grâce au *pipe* `|` directement après une valeur dans le template.
2. Les tags simples - ils peuvent prendre une valeur ou plusieurs en paramètre et retourne une nouvelle valeur. Pour les appeler, c'est *via* les tags `{% nom_de_la_fonction param1 param2 ... %}`.
3. Les tags d'inclusion: ils retournent un contexte (ie. un dictionnaire), qui est ensuite passé à un nouveau template.
1. *Les filtres* - on peut les appeler grâce au *pipe* `|` directement après une valeur dans le template.
2. *Les tags simples* - ils peuvent prendre une valeur ou plusieurs en paramètre et retourne une nouvelle valeur. Pour les appeler, c'est *via* les tags `{% nom_de_la_fonction param1 param2 ... %}`.
3. *Les tags d'inclusion*: ils retournent un contexte (ie. un dictionnaire), qui est ensuite passé à un nouveau template. Type `{% include '...' ... %}`.
Pour l'implémentation:
1. On prend l'application `wish` et on y ajoute un répertoire `templatetags`, ainsi qu'un fichier `__init__.py`.
2. Dans ce nouveau paquet, on ajoute un nouveau module que l'on va appeler `tools.py`
3. Dans ce module, pour avoir un aperçu des possibilités, on va définir trois fonctions (une pour chaque type de tags possible).
[source,bash]
----
[Inclure un tree du dossier template tags]
----
Pour plus d'informations, la https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#writing-custom-template-tags[documentation officielle est un bon début].
==== Filtres
[source,python]
----
# wish/tools.py
@ -114,14 +185,47 @@ register = template.Library()
@register.filter(is_safe=True)
def add_xx(value):
return '%sxx' % value
----
==== Tags simples
[source,python]
----
# wish/tools.py
from django import template
from wish.models import Wishlist
register = template.Library()
@register.simple_tag
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
----
==== Tags d'inclusion
[source,python]
----
# wish/tools.py
from django import template
from wish.models import Wishlist
register = template.Library()
@register.inclusion_tag('wish/templatetags/wishlists_list.html')
def wishlists_list():
return { 'list': Wishlist.objects.all() }
----
Pour plus d'informations, la https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#writing-custom-template-tags[documentation officielle est un bon début].

View File

@ -0,0 +1,92 @@
== URLs et espaces de noms
La gestion des URLs permet *grosso modo* d'assigner une adresse paramétrée ou non à une fonction Python. La manière simple consiste à modifier le fichier `gwift/settings.py` pour y ajouter nos correspondances. Par défaut, le fichier ressemble à ceci:
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]
----
La variable `urlpatterns` associe un ensemble d'adresses à des fonctions. Dans le fichier *nu*, seul le *pattern* `admin` est défini, et inclut toutes les adresses qui sont définies dans le fichier `admin.site.urls`.
NOTE: petit mot d'explication sur les expressions rationnelles.
[source,python]
----
# admin.site.urls.py
----
Pour reprendre l'exemple où on en était resté:
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from wish import views as wish_views
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', wish_views.wishlists, name='wishlists'),
]
----
A présent, on doit tester que l'URL racine de notre application mène bien vers la fonction `wish_views.wishlists`.
Prenons par exemple l'exemple de Twitter : quand on accède à une URL, elle est de la forme `https://twitter.com/<user>``. Sauf que les pages `about` et `help` existent également. Pour implémenter ce type de précédence, il faudrait implémenter les URLs de la manière suivante:
[source,text]
----
| about
| help
| <user>
----
Mais cela signifie aussi que les utilisateurs `about` et `help` (s'ils existent...) ne pourront jamais accéder à leur profil. Une dernière solution serait de maintenir une liste d'authorité des noms d'utilisateur qu'il n'est pas possible d'utiliser.
D'où l'importance de bien définir la séquence de déinition de ces routes, ainsi que des espaces de noms.
L'idée des espaces de noms ou _namespaces_ est de définir un _sous-répertoire_ dans lequel on trouvera nos nouvelles routes. Cette manière de procéder permet notamment de répondre au problème ci-dessous, en définissant un sous-dossier type `https://twitter.com/users/<user>``.
De là, découle une autre bonne pratique: l'utilisation de _breadcrumbs_ (https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template) ou de guidelines de navigation.
=== Reverse
En associant un nom ou un libellé à chaque URL, il est possible de récupérer sa *traduction*. Cela implique par contre de ne plus toucher à ce libellé par la suite...
Dans le fichier `urls.py`, on associe le libellé `wishlists` à l'URL `r'^$` (c'est-à-dire la racine du site):
[source,python]
----
from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]
----
De cette manière, dans nos templates, on peut à présent construire un lien vers la racine avec le tags suivant:
[source,html]
----
<a href="{% url 'wishlists' %}">{{ yearvar }} Archive</a>
----
De la même manière, on peut également récupérer l'URL de destination pour n'importe quel libellé, de la manière suivante:
[source,python]
----
from django.core.urlresolvers import reverse_lazy
wishlists_url = reverse_lazy('wishlists')
----

View File

@ -1,10 +1,10 @@
=== Vues
== Vues
Une vue correspond à un contrôleur dans le pattern MVC. Tout ce que vous pourrez définir au niveau du fichier `views.py` fera le lien entre le modèle stocké dans la base de données et ce avec quoi l'utilisateur pourra réellement interagir (le `template`).
Chaque vue peut etre représentée de deux manières: soit par des fonctions, soit par des classes. Le comportement leur est propre, mais le résultat reste identique. Le lien entre l'URL à laquelle l'utilisateur accède et son exécution est faite au travers du fichier `gwift/urls.py`, comme on le verra par la suite.
==== Function Based Views
=== Function Based Views
Les fonctions (ou `FBV` pour *Function Based Views*) permettent une implémentation classique des contrôleurs. Au fur et à mesure de votre implémentation, on se rendra compte qu'il y a beaucoup de répétitions dans ce type d'implémentation: elles ne sont pas obsolètes, mais dans certains cas, il sera préférable de passer par les classes.
@ -14,8 +14,6 @@ Pour définir la liste des `WishLists` actuellement disponibles, on précédera
. Construction d'une URL qui permettra de lier l'adresse à l'exécution de la fonction.
. Définition du squelette.
===== Définition de la fonction
[source,python]
----
# wish/views.py
@ -25,27 +23,13 @@ from .models import Wishlist
def wishlists(request):
w = Wishlist.objects.all()
return render(request, 'wish/list.html',{ 'wishlists': w })
return render(request, 'wish/list.html', { 'wishlists': w })
----
===== Construction de l'URL
Rien qu'ici, on doit déjà tester deux choses:
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from wish import views as wish_views
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', wish_views.wishlists, name='wishlists'),
]
----
===== Définition du squelette
. Qu'on construit bien le modèle attendu - la liste de tous les souhaits déjà émis.
. Que le template `wish/list.html` existe bien - sans quoi, on va tomber sur une erreur de type `TemplateDoesNotExist` dans notre environnement de test, et sur une erreur 500 en production.
A ce stade, vérifiez que la variable `TEMPLATES` est correctement initialisée dans le fichier `gwift/settings.py` et que le fichier `templates/wish/list.html` ressemble à ceci:
@ -69,8 +53,6 @@ A ce stade, vérifiez que la variable `TEMPLATES` est correctement initialisée
</html>
----
===== Exécution
A présent, ajoutez quelques listes de souhaits grâce à un *shell*, puis lancez le serveur:
[source,bash]
@ -90,20 +72,22 @@ Lancez le serveur grâce à la commande `python manage.py runserver`, ouvrez un
Rien de très sexy, aucune interaction avec l'utilisateur, très peu d'utilisation des variables contextuelles, mais c'est un bon début! =)
==== Class Based Views
=== Class Based Views
Les classes, de leur côté, implémente le *pattern* objet et permettent d'arriver facilement à un résultat en très peu de temps, parfois même en définissant simplement quelques attributs, et rien d'autre. Pour l'exemple, on va définir deux classes qui donnent exactement le même résultat que la fonction `wishlists` ci-dessus. Une première fois en utilisant une classe générique vierge, et ensuite en utilisant une classe de type `ListView`.
===== Classe générique
Voir https://ccbv.co.uk/[Classy Class Based Views].
blah
L'idée derrière les classes est de définir des fonctions *par convention plutôt que par configuration*.
===== ListView
NOTE: à compléter ici :-)
==== ListView
Les classes génériques implémentent un aspect bien particulier de la représentation d'un modèle, en utilisant très peu d'attributs. Les principales classes génériques sont de type `ListView`, [...]. L'implémentation consiste, exactement comme pour les fonctions, à:
. Définir la classe
. Créer l'URL
. Définir une sous-classe de celle que l'on souhaite utiliser
. Câbler l'URL qui lui sera associée
. Définir le squelette.
[source,python]
@ -120,6 +104,29 @@ class WishListList(ListView):
template_name = 'wish/list.html'
----
Il est même possible de réduire encore ce morceau de code en définissant juste le snippet suivant :
[source,python]
----
# wish/views.py
from django.views.generic import ListView
from .models import Wishlist
class WishListList(ListView):
context_object_name = 'wishlists'
----
Par inférence, Django construit beaucoup d'informations: si on n'avait pas spécifié les variables `context_object_name` et `template_name`, celles-ci auraient pris les valeurs suivantes:
* `context_object_name`: `wishlist_list` (ou plus précisément, le nom du modèle suivi de `_list`)
* `template_name`: `wish/wishlist_list.html` (à nouveau, le fichier généré est préfixé du nom du modèle).
En l'état, par rapport à notre précédente vue basée sur une fonction, on y gagne sur les conventions utilisées et le nombre de tests à réaliser. A vous de voir la déclaration que vous préférez, en fonction de vos affinités et du résultat que vous souhaitez atteindre.
NOTE: un petit tableau de différence entre les deux ? :-)
[source,python]
----
# gwift/urls.py
@ -135,7 +142,5 @@ urlpatterns = [
]
----
C'est tout. Lancez le serveur, le résultat sera identique. Par inférence, Django construit beaucoup d'informations: si on n'avait pas spécifié les variables `context_object_name` et `template_name`, celles-ci auraient pris les valeurs suivantes:
C'est tout. Lancez le serveur, le résultat sera identique.
* `context_object_name`: `wishlist_list` (ou plus précisément, le nom du modèle suivi de `_list`)
* `template_name`: `wish/wishlist_list.html` (à nouveau, le fichier généré est préfixé du nom du modèle).

View File

@ -0,0 +1,7 @@
= En Bonus
https://gto76.github.io/python-cheatsheet/[Python Cheat Sheet]
include::code-snippets.adoc[]
include::legacy.adoc[]

View File

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

View File

@ -1,8 +0,0 @@
extends: default
footer:
recto:
right:
content: '{section-or-chapter-title} | {page-number}'
verso:
left:
content: '{page-number} | {chapter-title}'

View File

@ -1,39 +0,0 @@
== Méthodologie de travail
Pour la méthode de travail et de développement, on va se baser sur les https://12factor.net/fr/[The Twelve-factor App] - ou plus simplement les *12 facteurs*.
L'idée derrière cette méthode consiste à pousser les concepts suivants (repris grossièrement de la https://12factor.net/fr/[page d'introduction] :
. Faciliter la mise en place de phases d'automatisation; plus simplement, faciliter les mises à jour applicatives, simplifier la gestion de l'hôte, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'https://en.wikipedia.org/wiki/Continuous_integration[intégration continue]/link:https://en.wikipedia.org/wiki/Continuous_deployment[déploiement continu]
. Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet.
. Faciliter
. Augmenter l'agilité générale du projet, en permettant une meilleure évolutivité architecturale et une meilleure mise à l'échelle - _Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on n'en parle plus ;-)_.
En pratique, les idées planquées derrière les quelques phrases ci-dessus permettront de monter facilement un nouvel environnement - qu'il soit sur la machine du petit nouveau ou sur un serveur Azure, Heroku, Digital Ocean ou votre nouveau Raspberry Pi Zéro caché à la cave.
Pour reprendre de manière très brute les différentes idées derrière cette méthode, on a:
NOTE: pensez à retravailler la partie ci-dessous; la version anglophone semble plus compréhensible... :-/
. Une base de code suivie avec un système de contrôle de version, plusieurs déploiements
. Déclarez explicitement et isolez les dépendances
. Stockez la configuration dans lenvironnement
. Traitez les services externes comme des ressources attachées
. Séparez strictement les étapes dassemblage et dexécution
. Exécutez lapplication comme un ou plusieurs processus sans état
. Exportez les services via des associations de ports
. Grossissez à laide du modèle de processus
. Maximisez la robustesse avec des démarrages rapides et des arrêts gracieux
. Gardez le développement, la validation et la production aussi proches que possible
. Traitez les logs comme des flux dévènements
. Lancez les processus dadministration et de maintenance comme des one-off-processes
.Concrètement
|===
|Concept |Outil |Description
|Base de code suivie avec un système de contrôle de version| Git, Mercurial, SVN, ...|Chaque déploiement démarre à partir d'une base de code unique. Il n'y pas de dépôt "Prod", "Staging" ou "Dev". Il n'y en a qu'un et un seul.
|Déclaration explicite et isolation des dépendances| Pyenv, environnements virtuels, RVM, ...|Afin de ne pas perturber les dépendances systèmes, chaque application doit disposer d'un environnement sain par défaut.
|Configuration dans l'environnement| Fichiers .ENV| Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, on ne doit trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code.
|Services externes = ressources locales| Fichiers .ENV| Chaque ressource doit pouvoir être interchangeable avec une autre, sans modification du code source. La solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement via des variables d'environnement.
|Bien séparer les étapes de construction des étapes de mise à disposition| Capistrano, Gitea, un serveur d'artefacts, ...| L'idée est de pouvoir récupérer une version spécifique du code, sans que celle-ci ne puisse avoir été modifiée. Git permet bien de gérer des versions (au travers des tags), mais ces éléments peuvent sans doute être modifiés directement au travers de l'historique.
|===

View File

@ -1,64 +0,0 @@
== Django
Comme on l'a vu ci-dessus, ``django-admin`` permet de créer un nouveau projet. On fait ici une distinction entre un **projet** et une **application**:
* **Projet**: ensemble des applications, paramètres, pages HTML, middlwares, dépendances, etc., qui font que votre code fait ce qu'il est sensé faire.
* **Application**: *contexte* éventuellement indépendant, permettant d'effectuer une partie isolée de ce que l'on veut faire.
Pour ``gwift``, on va notamment avoir une application pour la gestion des listes de souhaits et des éléments, une deuxième application pour la gestion des utilisateurs, voire une troisième application qui gérera les partages entre utilisateurs et listes.
On voit bien ici le principe de **contexte**: l'application viendra avec son modèle, ses tests, ses vues et son paramétrage. Elle pourra éventuellement être réutilisée dans un autre projet. C'est en ça que consistent les `paquets Django <https://www.djangopackages.com/>`_ déjà disponibles: ce sont simplement de petites applications empaquetées pour être réutilisées (eg. `Django-Rest-Framework <https://github.com/tomchristie/django-rest-framework>`_, `Django-Debug-Toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_, ...).
=== Gestion
Comme expliqué un peu plus haut, le fichier ``manage.py`` est un *wrapper* sur les commandes ``django-admin``. A partir de maintenant, nous n'utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet:
* ``manage.py check`` pour vérifier (en surface...) que votre projet ne rencontre aucune erreur
* ``manage.py runserver`` pour lancer un serveur de développement
* ``manage.py test`` pour découvrir les tests unitaires disponibles et les lancer.
La liste complète peut être affichée avec ``manage.py help``. Vous remarquerez que ces commandes sont groupées selon différentes catégories:
* **auth**: création d'un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.
* **django**: vérifier la *compliance* du projet, lancer un *shell*, *dumper* les données de la base, effectuer une migration du schéma, ...
* **sessions**: suppressions des sessions en cours
* **staticfiles**: gestion des fichiers statiques et lancement du serveur de développement.
Nous verrons plus tard comment ajouter de nouvelles commandes.
=== Structure d'une application
Maintenant que l'on a vu à quoi servait ``manage.py``, on peut créer notre nouvelle application grâce à la commande ``manage.py startapp <label>``.
Cette application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir. Essayez de trouver un nom éloquent, court et qui résume bien ce que fait l'application. Pour nous, ce sera donc ``wish``. C'est parti pour ``manage.py startapp wish``!
[source,bash]
----
$ python manage.py startapp wish
----
Résultat? Django nous a créé un répertoire ``wish``, dans lequel on trouve les fichiers suivants:
[source,bash]
----
$ ls -l wish
admin.py __init__.py migrations models.py tests.py views.py
----
En résumé, chaque fichier a la fonction suivante:
* ``admin.py`` servira à structurer l'administration de notre application. Chaque information peut en effet être administrée facilement au travers d'une interface générée à la volée par le framework. On y reviendra par la suite.
* ``__init__.py`` pour que notre répertoire ``wish`` soit converti en package Python.
* ``migrations/``, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* ``models.py`` pour représenter et structurer nos données.
* ``tests.py`` pour les tests unitaires.
=== Tests unitaires
Plein de trucs à compléter ici ;-) Est-ce qu'on passe par pytest ou par le framework intégré ? Quels sont les avantages de l'un % à l'autre ?
* ``views.py`` pour définir ce que nous pouvons faire avec nos données.

View File

@ -1,9 +0,0 @@
== En résumé
En résumé, la création d'un **nouveau** projet Django demande plus ou moins toujours les mêmes actions:
. Configurer un environnement virtuel
. Installer les dépendances et les ajouter dans le fichier ``requirements.txt``
. Configurer le fichier ``settings.py``
C'est ici que le projet http://cookiecutter.readthedocs.io/en/latest/readme.html[CookieCutter] va être intéressant: les X premières étapes peuvent être *bypassées* par une simple commande.

View File

@ -1,303 +0,0 @@
== Chaîne d'outils
Le langage Python fonctionne avec un système daméliorations basées sur des propositions: les PEP, ou “Python Enhancement Proposal”.
Celle qui va nous intéresser pour cette section est la https://www.python.org/dev/peps/pep-0008/[PEP 8 -- Style Guide for Python Code]. Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour lindentation, le nommage des variables et des classes, … En bref, elle décrit comment écrire du code proprement pour que dautres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
=== PEP8
Le langage Python fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le `Benevolent Dictator For Life <http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life>`_.
La PEP qui nous intéresse plus particulièrement pour la suite est la `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_, ou "Style Guide for Python Code". Elle spécifie des conventions d'organisation et de formatage de code Python, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, etc. En bref, elle décrit comment écrire du code proprement pour que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (``.``, le nom d'un répertoire, le nom d'un fichier ``.py``, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options ``--statistics -qq``.
.. code-block:: shell
$ pep8 . --statistics -qq
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez ``pyflakes``: il analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
Finalement, la solution qui couvre ces deux domaines existe et s'intitule `flake8 <https://github.com/PyCQA/flake8>`_. Sur base la même interface que ``pep8``, vous aurez en plus tous les avantages liés à ``pyflakes`` concernant votre code source.
==== PEP257
.. todo:: à remplir avec ``pydocstyle``.
==== Tests
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe ``TestCase`` depuis le package ``django.test``:
.. code-block:: python
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
==== Couverture de code
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet `coverage <https://pypi.python.org/pypi/coverage/>`_, qui se charge d'évaluer le pourcentage de code couvert par les tests. Ajoutez-le dans le fichier ``requirements/base.txt``, et lancez une couverture de code grâce à la commande ``coverage``. La configuration peut se faire dans un fichier ``.coveragerc`` que vous placerez à la racine de votre projet, et qui sera lu lors de l'exécution.
.. code-block:: shell
# requirements/base.text
[...]
coverage
django_coverage_plugin
.. code-block:: shell
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report
.. todo:: le bloc ci-dessous est à revoir pour isoler la configuration.
.. code-block:: shell
$ coverage run --source "." manage.py test
$ coverage report
Name Stmts Miss Cover
---------------------------------------------
gwift\gwift\__init__.py 0 0 100%
gwift\gwift\settings.py 17 0 100%
gwift\gwift\urls.py 5 5 0%
gwift\gwift\wsgi.py 4 4 0%
gwift\manage.py 6 0 100%
gwift\wish\__init__.py 0 0 100%
gwift\wish\admin.py 1 0 100%
gwift\wish\models.py 49 16 67%
gwift\wish\tests.py 1 1 0%
gwift\wish\views.py 6 6 0%
---------------------------------------------
TOTAL 89 32 64%
$ coverage html
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. Pour gagner un peu de temps, n'hésitez pas à créer un fichier ``Makefile`` que vous placerez à la racine du projet. L'exemple ci-dessous permettra, grâce à la commande ``make coverage``, d'arriver au même résultat que ci-dessus:
.. code-block:: shell
# Makefile for gwift
#
# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
$(error The 'coverage' command was not found. Make sure you have coverage installed)
endif
.PHONY: help coverage
help:
@echo " coverage to run coverage check of the source files."
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
==== Complexité de McCabe
La `complexité cyclomatique <https://fr.wikipedia.org/wiki/Nombre_cyclomatique>`_ (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:
.. code-block:: python
if True == True:
pass # never happens
# continue ...
La condition existe, mais on ne passera jamais dedans. A l'inverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:
.. code-block:: python
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusqu'à arriver à l'impression à l'écran et à la valeur de retour. La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. Si on rencontre une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour s'assurer que la couverture est complète. On devrait donc trouver:
1. Un test pour entrer (ou non) dans la condition ``a == b``
2. Un test pour entrer (ou non) dans la condition ``b == c``
3. Un test pour entrer (ou non) dans la condition ``c == d``
4. Un test pour entrer (ou non) dans la condition ``d == e``
5. Et s'assurer que n'importe quel autre cas retournera la valeur ``None``.
On a donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
.. note::
Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale
A nouveau, un greffon pour ``flake8`` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre ``--max-complexity``. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
==== Documentation
Il existe plusieurs manières de générer la documentation d'un projet. Les plus connues sont `Sphinx <http://sphinx-doc.org/>`_ et `MkDocs <http://www.mkdocs.org/>`_. Le premier a l'avantage d'être plus reconnu dans la communauté Python que l'autre, de pouvoir *parser* le code pour en extraire la documentation et de pouvoir lancer des `tests orientés documentation <https://duckduckgo.com/?q=documentation+driven+development&t=ffsb>`_. A contrario, votre syntaxe devra respecter `ReStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans son résultat.
Dans l'immédiat, nous nous contenterons d'avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/...). Dans la continuié de ``Flake8``, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.
.. note::
voir si il ne faudrait pas mieux passer par pydocstyle.
.. code-block:: shell
$ pip install flake8_docstrings
Lancez ensuite `flake8` avec la commande ``flake8 . --exclude="migrations"``. Sur notre projet (presque) vide, le résultat sera le suivant:
.. code-block:: shell
$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused
Bref, on le voit: nous n'avons que très peu de modules, et aucun d'eux n'est commenté.
En plus de cette méthode, Django permet également de rendre la documentation accessible depuis son interface d'administration.
=== pep8, flake8, pylint
Un outil existe et listera lensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour linstaller, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (., le nom dun répertoire, le nom dun fichier .py, ...).
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez pyflakes: il analaysera vos sources à la recherche derreurs (imports inutilsés, méthodes inconnues, etc.).
Et finalement, si vous voulez grouper les deux, il existe flake8. Sur base la même interface que pep8, vous aurez en plus des avertissements concernant le code source.
[source,python]
--
from datetime import datetime
"""On stocke la date du jour dans la variable ToD4y"""
ToD4y = datetime.today()
def print_today(ToD4y):
today = ToD4y
print(ToD4y)
def GetToday():
return ToD4y
if __name__ == "__main__":
t = Get_Today()
print(t)
--
L'exécution de la commande flake8 . retourne ceci:
[source,bash]
--
test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
test.py:18:1: W391 blank line at end of file
--
On trouve des erreurs:
* de *conventions*: le nombre de lignes qui séparent deux fonctions, le nombre d'espace après un opérateur, une ligne vide à la fin du fichier, ... Ces _erreurs_ n'en sont pas vraiment, elles indiquent juste de potentiels problèmes de communication si le code devait être lu ou compris par une autre personne.
* de *définition*: une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug potentiel.
L'étape d'après consiste à invoquer pylint. Lui, il est directement moins conciliant:
[source,text]
--
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10
--
En gros, j'ai programmé comme une grosse bouse anémique (et oui, le score d'évaluation du code permet bien d'aller en négatif). En vrac, on trouve des problèmes liés:
* au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)
* à des variables non définies (E0602)
* de la documentation manquante (C0114, C0116)
* de la redéfinition de variables (W0621).
Pour reprendre la http://pylint.pycqa.org/en/latest/user_guide/message-control.html[documentation], chaque code possède sa signification (ouf!):
* C convention related checks
* R refactoring related checks
* W various warnings
* E errors, for probable bugs in the code
* F fatal, if an error occurred which prevented pylint from doing further* processing.
PyLint est la version **++**, pour ceux qui veulent un code propre et sans bavure.
=== Black
=== pytest
=== mypy

View File

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