gwift-book/source/main.xml

7160 lines
432 KiB
XML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?xml version="1.0" encoding="UTF-8"?>
<?asciidoc-toc maxdepth="1"?>
<?asciidoc-numbered?>
<book xmlns="http://docbook.org/ns/docbook" xmlns:xl="http://www.w3.org/1999/xlink" version="5.0" xml:lang="en">
<info>
<title>Minor swing with Django</title>
<date>2022-02-20</date>
<authorgroup>
<author>
<personname>
<firstname>Cédric</firstname>
<surname>Declerfayt</surname>
</personname>
<email>jaguarondi27@gmail.com</email>
</author>
<author>
<personname>
<firstname>Fred</firstname>
<surname>Pauchet</surname>
</personname>
<email>fred@grimbox.be</email>
</author>
</authorgroup>
</info>
<preface xml:id="_licence">
<title>Licence</title>
<simpara>Ce travail est licencié sous Attribution-NonCommercial 4.0 International Attribution-NonCommercial 4.0 International</simpara>
<simpara>This license requires that reusers give credit to the creator.
It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, for noncommercial purposes only.</simpara>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">BY</emphasis>: Credit must be given to you, the creator.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">NC</emphasis>: Only noncommercial use of your work is permitted. Noncommercial means not primarily intended for or directed towards commercial advantage or monetary compensation.</simpara>
</listitem>
</itemizedlist>
<simpara><link xl:href="https://creativecommons.org/licenses/by-nc/4.0/?ref=chooser-v1">https://creativecommons.org/licenses/by-nc/4.0/?ref=chooser-v1</link></simpara>
<simpara>La seule exception concerne les morceaux de code (non attribués), disponibles sous licence <link xl:href="https://mit-license.org/">MIT</link>.</simpara>
</preface>
<preface xml:id="_préface">
<title>Préface</title>
<blockquote>
<attribution>
Robert C. Martin
</attribution>
<simpara>The only way to go fast, is to go well</simpara>
</blockquote>
<simpara>Nous n&#8217;allons pas vous mentir: il existe enormément de tutoriaux très bien réalisés sur "<emphasis>Comment réaliser une application Django</emphasis>" et autres "<emphasis>Déployer votre code en 2 minutes</emphasis>".
Nous nous disions juste que ces tutoriaux restaient relativement haut-niveaux et se limitaient à un contexte donné, sans réellement préparer à la maintenance et au suivi de l&#8217;application nouvellement développée.</simpara>
<simpara>L&#8217;idée du texte ci-dessous est de jeter les bases d&#8217;un bon développement, en survolant l&#8217;ensemble des outils permettant de suivre des lignes directrices reconnues, de maintenir une bonne qualité de code au travers des différentes étapes menant jusqu&#8217;au déploiement et de s&#8217;assurer du maintient correct de la base de code, en permettant à n&#8217;importe qui de reprendre ce qui aura déjà été écrit.</simpara>
<simpara>Ces idées ne s&#8217;appliquent pas uniquement à Django et à son cadre de travail, ni même au langage Python.
Ces deux sujets sont cependant de bons candidats et leur cadre de travail est bien défini, documenté et suffisamment flexible.</simpara>
<simpara>Django se présente comme un <emphasis>Framework Web pour perfectionnistes ayant des deadlines</emphasis> cite:[django] et suit ces quelques principes cite:[django_design_philosophies]:</simpara>
<itemizedlist>
<listitem>
<simpara>Faible couplage et forte cohésion, pour que chaque composant dispose de son indépendance, en n&#8217;ayant aucune connaissance des autres couches applicatives. Ainsi, le moteur de rendu ne connait absolument rien l&#8217;existence du moteur de base de données, tout comme le système de vues ne sait pas quel moteur de rendu est utilisé.</simpara>
</listitem>
<listitem>
<simpara>Plus de fonctionnalités avec moins de code: chaque application Django doit utiliser le moins de code possible</simpara>
</listitem>
<listitem>
<simpara><emphasis>Don&#8217;t repeat yourself</emphasis>, chaque concept ou morceau de code ne doit être présent qu&#8217;à un et un seul endroit de vos dépôts.</simpara>
</listitem>
<listitem>
<simpara>Rapidité du développement, en masquant les aspects fastidieux du développement web actuel</simpara>
</listitem>
</itemizedlist>
<simpara>Mis côte à côte, le suivi de ces principes permet une bonne stabilité du projet à moyen et long terme.</simpara>
<simpara>Comme nous le verrons par la suite, et sans être parfait, Django offre une énorme flexibilité qui permet de se laisser le maximum d&#8217;options ouvertes tout en permettant d&#8217;expérimenter facilement plusieurs pistes, jusqu&#8217;au moment de prendre une vraie décision.
Dans la majorité des cas problématiques pouvant être rencontrés lors du développement d&#8217;une application Web, Django proposera une solution pragmatique, compréhensible et facile à mettre en place.
En résumé de ce paragraphe, pour tout problème commun, vous disposerez d&#8217;une solution logique.
Tout pour plaire à n&#8217;importe quel directeur IT.</simpara>
<simpara><emphasis role="strong">Dans la première partie</emphasis>, nous verrons comment partir d&#8217;un environnement sain, comment le configurer correctement, comment installer Django de manière isolée et comment démarrer un nouveau projet.
Nous verrons rapidement comment gérer les dépendances, les versions et comment appliquer et suivre un score de qualité de notre code.
Ces quelques points pourront être appliqués pour n&#8217;importe quel langage ou cadre de travail.
Nous verrons aussi que la configuration proposée par défaut par le framework n&#8217;est pas idéale dans la majorité des cas.</simpara>
<simpara>Pour cela, nous présenterons différents outils, la rédaction de tests unitaires et d&#8217;intégration pour limiter les régressions, les règles de nomenclature et de contrôle du contenu, comment partir d&#8217;un squelette plus complet, ainsi que les bonnes étapes à suivre pour arriver à un déploiement rapide et fonctionnel avec peu d&#8217;efforts.</simpara>
<simpara>A la fin de cette partie, vous disposerez d&#8217;un code propre et d&#8217;un projet fonctionnel, bien qu&#8217;encore un peu inutile.</simpara>
<simpara><emphasis role="strong">Dans la deuxième partie</emphasis>, nous aborderons les grands principes de modélisation, en suivant les lignes de conduites du cadre de travail.
Nous aborderons les concepts clés qui permettent à une application de rester maintenable, les formulaires, leurs validations, comment gérer les données en entrée, les migrations de données et l&#8217;administration.</simpara>
<simpara><emphasis role="strong">Dans la troisième partie</emphasis>, nous détaillerons précisément les étapes de déploiement, avec la description et la configuration de l&#8217;infrastructure, des exemples concrets de mise à disposition sur deux distributions principales (Debian et CentOS), sur une <emphasis>*Plateform as a Service*</emphasis>, ainsi que l&#8217;utilisation de Docker et Docker-Compose.</simpara>
<simpara>Nous aborderons également la supervision et la mise à jour d&#8217;une application existante, en respectant les bonnes pratiques d&#8217;administration système.</simpara>
<simpara><emphasis role="strong">Dans la quatrième partie</emphasis>, nous aborderons les architectures typées <emphasis>entreprise</emphasis>, les services et les différentes manières de structurer notre application pour faciliter sa gestion et sa maintenance, tout en décrivant différents types de scénarii, en fonction des consommateurs de données.</simpara>
<simpara><emphasis role="strong">Dans la cinquième partie</emphasis>, nous mettrons ces concepts en pratique en présentant le développement en pratique de deux applications, avec la description de problèmes rencontrés et la solution qui a été choisie: définition des tables, gestion des utilisateurs, &#8230;&#8203; et mise à disposition.</simpara>
<section xml:id="_pour_qui">
<title>Pour qui ?</title>
<simpara>Avant tout, pour moi.
Comme le disait le Pr Richard Feynman: "<emphasis>Si vous ne savez pas expliquer quelque chose simplement, c&#8217;est que vous ne l&#8217;avez pas compris</emphasis>".
<footnote><simpara>Et comme l&#8217;ajoutait Aurélie Jean dans de L&#8217;autre côté de la machine: <emphasis>"Si personne ne vous pose de questions suite à votre explication, c&#8217;est que vous n&#8217;avez pas été suffisamment clair !"</emphasis> cite:[other_side</simpara></footnote>]</simpara>
<simpara>Ce livre s&#8217;adresse autant au néophyte qui souhaite se lancer dans le développement Web qu&#8217;à l&#8217;artisan qui a besoin d&#8217;un aide-mémoire et qui ne se rappelle plus toujours du bon ordre des paramètres, ou à l&#8217;expert qui souhaiterait avoir un aperçu d&#8217;une autre technologie que son domaine privilégié de compétences.</simpara>
<simpara>Beaucoup de concepts présentés peuvent être oubliés ou restés inconnus jusqu&#8217;au moment où ils seront réellement nécessaires.
A ce moment-là, pour peu que votre mémoire ait déjà entraperçu le terme, il vous sera plus facile d&#8217;y revenir et de l&#8217;appliquer.</simpara>
</section>
<section xml:id="_pour_aller_plus_loin">
<title>Pour aller plus loin</title>
<simpara>Il existe énormément de ressources, autant spécifiques à Django que plus généralistes.
Il ne sera pas possible de toutes les détailler; faites un tour sur</simpara>
<itemizedlist>
<listitem>
<simpara><link xl:href="https://duckduckgo.com">https://duckduckgo.com</link>,</simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://stackoverflow.com">https://stackoverflow.com</link>,</simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://ycombinator.com">https://ycombinator.com</link>,</simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://lobste.rs/">https://lobste.rs/</link>,</simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://lecourrierduhacker.com/">https://lecourrierduhacker.com/</link></simpara>
</listitem>
<listitem>
<simpara>ou <link xl:href="https://www.djangoproject.com/">https://www.djangoproject.com/</link>.</simpara>
</listitem>
</itemizedlist>
<simpara>Restez curieux, ne vous enclavez pas dans une technologie en particulier et gardez une bonne ouverture d&#8217;esprit.</simpara>
</section>
<section xml:id="_conventions">
<title>Conventions</title>
<note>
<simpara>Les notes indiquent des anecdotes.</simpara>
</note>
<tip>
<simpara>Les conseils indiquent des éléments utiles, mais pas spécialement indispensables.</simpara>
</tip>
<important>
<simpara>Les notes importantes indiquent des éléments à retenir.</simpara>
</important>
<caution>
<simpara>Ces éléments indiquent des points d&#8217;attention. Les retenir vous fera gagner du temps en débuggage.</simpara>
</caution>
<warning>
<simpara>Les avertissements indiquent un (potentiel) danger ou des éléments pouvant amener des conséquences pas spécialement sympathiques.</simpara>
</warning>
<simpara>Les morceaux de code source seront présentés de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered"># &lt;folder&gt;/&lt;fichier&gt;.&lt;extension&gt; <co xml:id="CO1-1"/>
def function(param):
""" <co xml:id="CO1-2"/>
"""
callback() <co xml:id="CO1-3"/></programlisting>
<calloutlist>
<callout arearefs="CO1-1">
<para>L&#8217;emplacement du fichier ou du morceau de code présenté, sous forme de commentaire</para>
</callout>
<callout arearefs="CO1-2">
<para>Des commentaires, si cela s&#8217;avère nécessaire</para>
</callout>
<callout arearefs="CO1-3">
<para>Les parties importantes ou récemment modifiées seront surlignées.</para>
</callout>
</calloutlist>
<tip>
<simpara>La plupart des commandes qui seront présentées dans ce livre le seront depuis un shell sous GNU/Linux.
Certaines d&#8217;entre elles pourraient devoir être adaptées si vous utilisez un autre système d&#8217;exploitation (macOS) ou n&#8217;importe quelle autre grosse bouse commerciale.</simpara>
</tip>
</section>
</preface>
<part xml:id="_environnement_et_méthodes_de_travail">
<title>Environnement et méthodes de travail</title>
<partintro>
<blockquote>
<attribution>
Kent Beck
</attribution>
<simpara>Make it work, make it right, make it fast</simpara>
</blockquote>
<simpara>En fonction de vos connaissances et compétences, la création dune nouvelle application est uneé tape relativement
facile à mettre en place.
Le code qui permet de faire tourner cette application peut ne pas être élégant, voire buggé jusqu&#8217;à la moëlle,
il pourra fonctionner et faire "preuve de concept".</simpara>
<simpara>Les problèmes arriveront lorsqu&#8217;une nouvelle demande sera introduite, lorsqu&#8217;un bug sera découvert et devra être corrigé
ou lorsqu&#8217;une dépendance cessera de fonctionner ou d&#8217;être disponible.
Or, une application qui névolue pas, meurt.
Tout application est donc destinée, soit à être modifiée, corrigée et suivie, soit à déperrir et à être délaissée
par ses utilisateurs.
Et cest juste cette maintenance qui est difficile.</simpara>
<simpara>Lapplication des principes présentés et agrégés ci-dessous permet surtout de préparer correctement tout ce qui pourra arriver,
sans aller jusquau « <emphasis role="strong">You Ain&#8217;t Gonna Need It</emphasis> » (ou <emphasis role="strong">YAGNI</emphasis>), qui consiste à surcharger tout développement avec des fonctionnalités non demandées, juste « au cas ou ».
Pour paraphraser une partie de lintroduction du livre <emphasis>Clean Architecture</emphasis> cite:[clean_architecture]:</simpara>
<blockquote>
<attribution>
Robert C. Martin
<citetitle>Clean Architecture</citetitle>
</attribution>
<simpara>Getting software right is hard: it takes knowledge and skills that most young programmers dont take the time to develop.
It requires a level of discipline and dedication that most programmers never dreamed theyd need.
Mostly, it takes a passion for the craft and the desire to be a professional.</simpara>
</blockquote>
<simpara>Le développement d&#8217;un logiciel nécessite une rigueur d&#8217;exécution et des connaissances précises dans des domaines extrêmement variés.
Il nécessite également des intentions, des (bonnes) décisions et énormément d&#8217;attention.
Indépendamment de l&#8217;architecture que vous aurez choisie, des technologies que vous aurez patiemment évaluées et mises en place, une architecture et une solution peuvent être cassées en un instant, en même temps que tout ce que vous aurez construit, dès que vous en aurez détourné le regard.</simpara>
<simpara>Un des objectifs ici est de placer les barrières et les gardes-fous (ou plutôt, les "<emphasis role="strong">garde-vous</emphasis>"), afin de péréniser au maximum les acquis, stabiliser les bases de tous les environnements (du développement à la production) qui accueilliront notre application et fiabiliser ainsi chaque étape de communication.</simpara>
<simpara>Dans cette partie-ci, nous parlerons de <emphasis role="strong">méthodes de travail</emphasis>, avec comme objectif d&#8217;éviter que l&#8217;application ne tourne que sur notre machine et que chaque déploiement ne soit une plaie à gérer.
Chaque mise à jour doit être réalisable de la manière la plus simple possible, et chaque étape doit être rendue la plus automatisée/automatisable possible.
Dans son plus simple élément, une application pourrait être mise à jour simplement en envoyant son code sur un dépôt centralisé: ce déclencheur doit démarrer une chaîne de vérification d&#8217;utilisabilité/fonctionnalités/débuggabilité/sécurité, pour immédiatement la mettre à disposition de nouveaux utilisateurs si toute la chaîne indique que tout est OK.
D&#8217;autres mécanismes fonctionnent également, mais au plus les actions nécessitent d&#8217;actions humaines, voire d&#8217;intervenants humains, au plus la probabilité qu&#8217;un problème survienne est grande.</simpara>
<simpara>Dans une version plus manuelle, cela pourrait se résumer à ces trois étapes (la dernière étant formellement facultative):</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Démarrer un script,</simpara>
</listitem>
<listitem>
<simpara>Prévoir un rollback si cela plante (et si cela a planté, préparer un post-mortem de l&#8217;incident pour qu&#8217;il ne se produise plus)</simpara>
</listitem>
<listitem>
<simpara>Se préparer une tisane en regardant nos flux RSS (pour peu que cette technologie existe encore&#8230;&#8203;).</simpara>
</listitem>
</orderedlist>
</partintro>
<chapter xml:id="_poésie_de_la_programmation">
<title>Poésie de la programmation</title>
<section xml:id="_complexité_de_mccabe">
<title>Complexité de McCabe</title>
<simpara>La <link xl:href="https://fr.wikipedia.org/wiki/Nombre_cyclomatique">complexité cyclomatique</link> (ou complexité de McCabe) peut s&#8217;apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d&#8217;embranchements trouvés dans une même section.
Quand le cycle d&#8217;exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite.</simpara>
<simpara>Par exemple:</simpara>
<programlisting language="python" linenumbering="unnumbered">if True == False:
pass # never happens
# continue ...</programlisting>
<simpara>TODO: faut vraiment reprendre un cas un peu plus lisible. Là, c&#8217;est naze.</simpara>
<simpara>La condition existe, mais nous ne passerons jamais dedans.
A l&#8217;inverse, le code suivant aura une complexité moisie à cause du nombre de conditions imbriquées:</simpara>
<programlisting language="python" linenumbering="unnumbered">def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1</programlisting>
<simpara>Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de cinq:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>le cas par défaut (a est différent de b, rien ne se passe),</simpara>
</listitem>
<listitem>
<simpara>le cas où <literal>a</literal> est égal à <literal>b</literal>, mais où <literal>b</literal> est différent de <literal>c</literal></simpara>
</listitem>
<listitem>
<simpara>le cas où <literal>a</literal> est égal à <literal>b</literal>, <literal>b</literal> est égal à <literal>c</literal>, mais <literal>c</literal> est différent de <literal>d</literal></simpara>
</listitem>
<listitem>
<simpara>le cas où <literal>a</literal> est égal à <literal>b</literal>, <literal>b</literal> est égal à <literal>c</literal>, <literal>c</literal> est égal à <literal>d</literal>, mais <literal>d</literal> est différent de <literal>e</literal></simpara>
</listitem>
<listitem>
<simpara>le cas où <literal>a</literal> est égal à <literal>b</literal>, <literal>b</literal> est égal à <literal>c</literal>, <literal>c</literal> est égal à <literal>d</literal> et <literal>d</literal> est égal à <literal>e</literal></simpara>
</listitem>
</orderedlist>
<simpara>La complexité cyclomatique d&#8217;un bloc est évaluée sur base du nombre d&#8217;embranchements possibles; par défaut, sa valeur est de 1.
Si nous rencontrons une condition, elle passera à 2, etc.</simpara>
<simpara>Pour l&#8217;exemple ci-dessous, nous allons devoir vérifier au moins chacun des cas pour nous assurer que la couverture est complète.
Nous devrions donc trouver:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Un test où rien de se passe (<literal>a != b</literal>)</simpara>
</listitem>
<listitem>
<simpara>Un test pour entrer dans la condition <literal>a == b</literal></simpara>
</listitem>
<listitem>
<simpara>Un test pour entrer dans la condition <literal>b == c</literal></simpara>
</listitem>
<listitem>
<simpara>Un test pour entrer dans la condition <literal>c == d</literal></simpara>
</listitem>
<listitem>
<simpara>Un test pour entrer dans la condition <literal>d == e</literal></simpara>
</listitem>
</orderedlist>
<simpara>Nous avons donc bien besoin de minimum cinq tests pour couvrir l&#8217;entièreté des cas présentés.</simpara>
<simpara>Le nombre de tests unitaires nécessaires à la couverture d&#8217;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&#8217;autres de 5.</simpara>
<simpara>Il est important de noter que refactoriser un bloc pour en extraire une méthode n&#8217;améliorera pas la complexité cyclomatique globale de l&#8217;application.
Nous visons ici une amélioration <emphasis role="strong">locale</emphasis>.</simpara>
</section>
<section xml:id="_conclusion">
<title>Conclusion</title>
<blockquote>
<attribution>
Robert C. Martin
</attribution>
<simpara>The primary cost of maintenance is in spelunking and risk cite:[clean_architecture(139)]</simpara>
</blockquote>
<simpara>En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite, lidée est de pousser le développement jusquau point où un service pourrait être nécessaire.
A ce stade, larchitecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe.
Nous nallons donc pas jusquau point où le service doit être créé (même sil peut ne pas être nécessaire),
ni à lextrême au fait dignorer quun service pourrait être nécessaire, mais nous aboutissons à une forme de compromis.
Une forme de comportement de Descartes, qui ne croit pas en Dieu, mais qui envisage quand même cette possibilité,
ce qui lui ouvre le maximum de portes 🙃</simpara>
<simpara>Avec cette approche, les composants sont déjà découplés au niveau du code source, ce qui pourrait savérer suffisant
jusquau stade où une modification ne pourra plus faire reculer léchéance.</simpara>
<simpara>En terme de découpe, les composants peuvent lêtre aux niveaux suivants:</simpara>
<itemizedlist>
<listitem>
<simpara>Code source</simpara>
</listitem>
<listitem>
<simpara>Déploiement, au travers de dll, jar, linked libraries, … voire au travers de threads ou de processus locaux.</simpara>
</listitem>
<listitem>
<simpara>Services</simpara>
</listitem>
</itemizedlist>
<simpara>Cette section se base sur deux ressources principales cite:[maintainable_software] cite:[clean_code], qui répartissent un ensemble de conseils parmi quatre niveaux de composants:</simpara>
<itemizedlist>
<listitem>
<simpara>Les méthodes et fonctions</simpara>
</listitem>
<listitem>
<simpara>Les classes</simpara>
</listitem>
<listitem>
<simpara>Les composants</simpara>
</listitem>
<listitem>
<simpara>Et des conseils plus généraux.</simpara>
</listitem>
</itemizedlist>
<simpara>Ces conseils sont valables pour n&#8217;importe quel langage.</simpara>
<section xml:id="_au_niveau_des_méthodes_et_fonctions">
<title>Au niveau des méthodes et fonctions</title>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">Gardez vos méthodes/fonctions courtes</emphasis>. Pas plus de 15 lignes, en comptant les commentaires. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9% de plus de 60 lignes; pas plus de 22.3% de plus de 30 lignes, au plus 43.7% de plus de 15 lignes et au moins 56.3% en dessous de 15 lignes). Oui, c&#8217;est dur à tenir, mais faisable.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Conserver une complexité de McCabe en dessous de 5</emphasis>, c&#8217;est-à-dire avec quatre branches au maximum. A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions ayant chacune une complexité de 5 conservera la complexité globale à 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">N&#8217;écrivez votre code qu&#8217;une seule fois: évitez les duplications, copie, etc.</emphasis>, c&#8217;est juste mal: imaginez qu&#8217;un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées. C&#8217;est aussi une forme de régression.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Conservez de petites interfaces</emphasis>. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester.</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_au_niveau_des_classes">
<title>Au niveau des classes</title>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">Privilégiez un couplage faible entre vos classes</emphasis>. Ceci n&#8217;est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences respectif. L&#8217;implémentation du service <literal>UserNotificationsService</literal> ne doit pas forcément se trouver embarqué dans une classe <literal>UserService</literal>. De même, pensez à passer par une interface (commune à plusieurs classes), afin d&#8217;ajouter une couche d&#8217;abstraction. La classe appellante n&#8217;aura alors que les méthodes offertes par l&#8217;interface comme points d&#8217;entrée.</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_au_niveau_des_composants">
<title>Au niveau des composants</title>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants</emphasis> également. Une manière d&#8217;arriver à ce résultat est de conserver un nombre de points d&#8217;entrée restreint, et d&#8217;éviter qu&#8217;il ne soit possible de contacter trop facilement des couches séparées de l&#8217;architecture. Pour une architecture n-tiers par exemple, la couche d&#8217;abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n&#8217;importe quelle couche de présentation risque de contacter directement la base de données, "<emphasis>juste parce qu&#8217;elle en a la possibilité</emphasis>". Vous pourriez également passer par des interfaces, afin de réduire le nombre de points d&#8217;entrée connus par un composant externe (qui ne connaîtra par exemple que <literal>IFileTransfer</literal> avec ses méthodes <literal>put</literal> et <literal>get</literal>, et non pas les détails d&#8217;implémentation complet d&#8217;une classe <literal>FtpFileTransfer</literal> ou <literal>SshFileTransfer</literal>).</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Conserver un bon balancement au niveau des composants</emphasis>: évitez qu&#8217;un composant <emphasis role="strong">A</emphasis> ne soit un énorme mastodonte, alors que le composant juste à côté ne soit capable que d&#8217;une action. De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités seront plus faciles à gérer. Un conseil est d&#8217;avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que chacun de ces composants soit approximativement de même taille.</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_de_manière_plus_générale">
<title>De manière plus générale</title>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">Conserver une densité de code faible</emphasis>: il n&#8217;est évidemment pas possible d&#8217;implémenter n&#8217;importe quelle nouvelle fonctionnalité en moins de 20 lignes de code; l&#8217;idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisant du refactoring (intensif?), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux. Mais surtout, en évitant de copier/coller bêtement du code existant.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Automatiser les tests</emphasis>, <emphasis role="strong">ajouter un environnement d&#8217;intégration continue dès le début du projet</emphasis> et <emphasis role="strong">vérifier par des outils les points ci-dessus</emphasis>.</simpara>
</listitem>
</itemizedlist>
<simpara>Unresolved directive in part-1-workspace/_main.adoc - include::mccabe.adoc[]</simpara>
</section>
</section>
</chapter>
<chapter xml:id="_fiabilité_évolutivité_et_maintenabilité">
<title>Fiabilité, évolutivité et maintenabilité</title>
<section xml:id="_12_facteurs">
<title>12 facteurs</title>
<simpara>Pour la méthode de travail et de développement, nous allons nous baser sur les <link xl:href="https://12factor.net/fr/">The Twelve-factor App</link> - ou plus simplement les <emphasis role="strong">12 facteurs</emphasis>.</simpara>
<simpara>L&#8217;idée derrière cette méthode, et indépendamment des langages de développement utilisés, consiste à suivre un ensemble de douze concepts, afin de:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara><emphasis role="strong">Faciliter la mise en place de phases d&#8217;automatisation</emphasis>; plus concrètement, de faciliter les mises à jour applicatives, simplifier la gestion de l&#8217;hôte, diminuer la divergence entre les différents environnements d&#8217;exécution et offrir la possibilité d&#8217;intégrer le projet dans un processus d&#8217;<link xl:href="https://en.wikipedia.org/wiki/Continuous_integration">intégration continue</link> ou <link xl:href="https://en.wikipedia.org/wiki/Continuous_deployment">déploiement continu</link></simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet</emphasis>, dans la mesure où la mise à disposition d&#8217;un environnement sera grandement facilitée.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Minimiser les divergences entre les différents environnemens sur lesquels un projet pourrait être déployé</emphasis></simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Augmenter l&#8217;agilité générale du projet</emphasis>, en permettant une meilleure évolutivité architecturale et une meilleure mise à l&#8217;échelle - <emphasis>Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on n&#8217;en parle plus ;-)</emphasis>.</simpara>
</listitem>
</orderedlist>
<simpara>En pratique, les points ci-dessus permettront de monter facilement un nouvel environnement - qu&#8217;il soit sur la machine du petit nouveau dans l&#8217;équipe, sur un serveur Azure/Heroku/Digital Ocean ou votre nouveau Raspberry Pi Zéro caché à la cave - et vous feront gagner un temps précieux.</simpara>
<simpara>Pour reprendre de manière très brute les différentes idées derrière cette méthode, nous avons:</simpara>
<simpara><emphasis role="strong">#1 - Une base de code unique, suivie par un système de contrôle de versions</emphasis>.</simpara>
<simpara>Chaque déploiement de l&#8217;application se basera sur cette source, afin de minimiser les différences que l&#8217;on pourrait trouver entre deux environnements d&#8217;un même projet. On utilisera un dépôt Git - Github, Gitlab, Gitea, &#8230;&#8203; Au choix.</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/diagrams/12-factors-1.png" align="center"/>
</imageobject>
<textobject><phrase>12 factors 1</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>Comme l&#8217;explique Eran Messeri, ingénieur dans le groupe Google Developer Infrastructure, un des avantages d&#8217;utiliser un dépôt unique de sources, est qu&#8217;il permet un accès facile et rapide à la forme la plus à jour du code, sans aucun besoin de coordination. <footnote><simpara>The DevOps Handbook, Part V, Chapitre 20, Convert Local Discoveries into Global Improvements</simpara></footnote>
Ce dépôt ne sert pas seulement au code source, mais également à d&#8217;autres artefacts et formes de connaissance:</simpara>
<itemizedlist>
<listitem>
<simpara>Standards de configuration (Chef recipes, Puppet manifests, &#8230;&#8203;)</simpara>
</listitem>
<listitem>
<simpara>Outils de déploiement</simpara>
</listitem>
<listitem>
<simpara>Standards de tests, y compris tout ce qui touche à la sécurité</simpara>
</listitem>
<listitem>
<simpara>Outils de déploiement de pipeline</simpara>
</listitem>
<listitem>
<simpara>Outils d&#8217;analyse et de monitoring</simpara>
</listitem>
<listitem>
<simpara>Tutoriaux</simpara>
</listitem>
</itemizedlist>
<simpara><emphasis role="strong">#2 - Déclarez explicitement les dépendances nécessaires au projet, et les isoler du reste du système lors de leur installation</emphasis></simpara>
<simpara>Chaque installation ou configuration doit toujours être faite de la même manière, et doit pouvoir être répétée quel que soit l&#8217;environnement cible.</simpara>
<simpara>Cela permet d&#8217;éviter que l&#8217;application n&#8217;utilise une dépendance qui soit déjà installée sur un des sytèmes de développement, et qu&#8217;elle soit difficile, voire impossible, à répercuter sur un autre environnement.
Dans notre cas, cela pourra être fait au travers de <link xl:href="https://pypi.org/project/pip/">PIP - Package Installer for Python</link> ou <link xl:href="https://python-poetry.org/">Poetry</link>.</simpara>
<simpara>Mais dans tous les cas, chaque application doit disposer d&#8217;un environnement sain, qui lui est assigné, et vu le peu de ressources que cela coûte, il ne faut pas s&#8217;en priver.</simpara>
<simpara>Chaque dépendance pouvant être déclarée et épinglée dans un fichier, il suffira de créer un nouvel environment vierge, puis d&#8217;utiliser ce fichier comme paramètre pour installer les prérequis au bon fonctionnement de notre application et vérifier que cet environnement est bien reproductible.</simpara>
<warning>
<simpara>Il est important de bien "épingler" les versions liées aux dépendances de l&#8217;application. Cela peut éviter des effets de bord comme une nouvelle version d&#8217;une librairie dans laquelle un bug aurait pu avoir été introduit. Parce qu&#8217;il arrive que ce genre de problème apparaisse, et lorsque ce sera le cas, ce sera systématiquement au mauvais moment.</simpara>
</warning>
<simpara><emphasis role="strong">#3 - Sauver la configuration directement au niveau de l&#8217;environnement</emphasis></simpara>
<simpara>Nous voulons éviter d&#8217;avoir à recompiler/redéployer l&#8217;application parce que:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>l&#8217;adresse du serveur de messagerie a été modifiée,</simpara>
</listitem>
<listitem>
<simpara>un protocole a changé en cours de route</simpara>
</listitem>
<listitem>
<simpara>la base de données a été déplacée</simpara>
</listitem>
<listitem>
<simpara>&#8230;&#8203;</simpara>
</listitem>
</orderedlist>
<simpara>En pratique, toute information susceptible de modifier un lien vers une ressource annexe doit se trouver dans un fichier ou dans une variable d&#8217;environnement, et doit être facilement modifiable.
En allant un pas plus loin, ceci de paramétrer facilement un environnement (par exemple, un container), simplement en modifiant une variable de configuration qui spécifierait la base de données sur laquelle l&#8217;application devra se connecter.</simpara>
<simpara>Toute clé de configuration (nom du serveur de base de données, adresse d&#8217;un service Web externe, clé d&#8217;API pour l&#8217;interrogation d&#8217;une ressource, &#8230;&#8203;) sera définie directement au niveau de l&#8217;hôte - à aucun moment, nous ne devons trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d&#8217;évoluer, écrite en dur dans le code.</simpara>
<simpara>Au moment de développer une nouvelle fonctionnalité, réfléchissez si l&#8217;un des composants utilisés risquerait de subir une modification: ce composant peut concerner une nouvelle chaîne de connexion, un point de terminaison nécessaire à télécharger des données officielles ou un chemin vers un répertoire partagé pour y déposer un fichier.</simpara>
<simpara><emphasis role="strong">#4 - Traiter les ressources externes comme des ressources attachées</emphasis></simpara>
<simpara>Nous parlons de bases de données, de services de mise en cache, d&#8217;API externes, &#8230;&#8203;
L&#8217;application doit être capable d&#8217;effectuer des changements au niveau de ces ressources sans que son code ne soit modifié. Nous parlons alors de <emphasis role="strong">ressources attachées</emphasis>, dont la présence est nécessaire au bon fonctionnement de l&#8217;application, mais pour lesquelles le <emphasis role="strong">type</emphasis> n&#8217;est pas obligatoirement défini.</simpara>
<simpara>Nous voulons par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached".
De cette manière, les ressources peuvent être attachées et détachées d&#8217;un déploiement à la volée.</simpara>
<simpara>Si une base de données ne fonctionne pas correctement (problème matériel ?), l&#8217;administrateur pourrait simplement restaurer un nouveau serveur à partir d&#8217;une précédente sauvegarde, et l&#8217;attacher à l&#8217;application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d&#8217;authentification, &#8230;&#8203;) directement <emphasis>via</emphasis> des variables d&#8217;environnement.</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/12factors/attached-resources.png" align="center"/>
</imageobject>
<textobject><phrase>attached resources</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>Nous serons ravis de pouvoir simplement modifier une chaîne <literal>sqlite:////tmp/my-tmp-sqlite.db'</literal> en <literal>psql://user:pass@127.0.0.1:8458/db</literal> lorsque ce sera nécessaire, sans avoir à recompiler ou redéployer les modifications.</simpara>
<simpara><emphasis role="strong">#5 - Séparer proprement les phases de construction, de mise à disposition et d&#8217;exécution</emphasis></simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>La <emphasis role="strong">construction</emphasis> (<emphasis>build</emphasis>) convertit un code source en un ensemble de fichiers exécutables, associé à une version et à une transaction dans le système de gestion de sources.</simpara>
</listitem>
<listitem>
<simpara>La <emphasis role="strong">mise à disposition</emphasis> (<emphasis>release</emphasis>) associe cet ensemble à une configuration prête à être exécutée,</simpara>
</listitem>
<listitem>
<simpara>tandis que la phase d'<emphasis role="strong">exécution</emphasis> (<emphasis>run</emphasis>) démarre les processus nécessaires au bon fonctionnement de l&#8217;application.</simpara>
</listitem>
</orderedlist>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/12factors/release.png" align="center"/>
</imageobject>
<textobject><phrase>release</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>Parmi les solutions possibles, nous pourrions nous pourrions nous baser sur les <emphasis>releases</emphasis> de Gitea, sur un serveur d&#8217;artefacts ou sur <link xl:href="https://fr.wikipedia.org/wiki/Capistrano_(logiciel)">Capistrano</link>.</simpara>
<simpara><emphasis role="strong">#6 - Les processus d&#8217;exécution ne doivent rien connaître ou conserver de l&#8217;état de l&#8217;application</emphasis></simpara>
<simpara>Toute information stockée en mémoire ou sur disque ne doit pas altérer le comportement futur de l&#8217;application, par exemple après un redémarrage non souhaité.</simpara>
<simpara>Pratiquement, si l&#8217;application devait rencontrer un problème, l&#8217;objectif est de pouvoir la redémarrer rapidement sur un autre serveur (par exemple suite à un problème matériel).
Toute information qui aurait été stockée durant l&#8217;exécution de l&#8217;application sur le premier hôte serait donc perdue.
Si une réinitialisation devait être nécessaire, l&#8217;application ne devra pas compter sur la présence d&#8217;une information au niveau du nouveau système.
La solution consiste donc à jouer sur les variables d&#8217;environnement (cf. #3) et sur les informations que l&#8217;on pourra trouver au niveau des ressources attachées (cf #4).</simpara>
<simpara>Il serait également difficile d&#8217;appliquer une mise à l&#8217;échelle de l&#8217;application, en ajoutant un nouveau serveur d&#8217;exécution, si une donnée indispensable à son fonctionnement devait se trouver sur la seule machine où elle est actuellement exécutée.</simpara>
<simpara><emphasis role="strong">#7 - Autoriser la liaison d&#8217;un port de l&#8217;application à un port du système hôte</emphasis></simpara>
<simpara>Les applications 12-factors sont auto-contenues et peuvent fonctionner en autonomie totale.
Elles doivent être joignables grâce à un mécanisme de ponts, où l&#8217;hôte qui s&#8217;occupe de l&#8217;exécution effectue lui-même la redirection vers l&#8217;un des ports ouverts par l&#8217;application, typiquement, en HTTP ou via un autre protocole.</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/diagrams/12-factors-7.png" align="center"/>
</imageobject>
<textobject><phrase>12 factors 7</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara><emphasis role="strong">#8 - Faites confiance aux processus systèmes pour l&#8217;exécution de l&#8217;application</emphasis></simpara>
<simpara>Comme décrit plus haut (cf. #6), l&#8217;application doit utiliser des processus <emphasis>stateless</emphasis> (sans état).
Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des processus particuliers pour certaines tâches: requêtes HTTP <emphasis>via</emphasis> des processus Web; <emphasis>long-running</emphasis> jobs pour des processus asynchrones, &#8230;&#8203;
Si cela existe au niveau du système, ne vous fatiguez pas: utilisez le.</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/12factors/process-types.png" align="center"/>
</imageobject>
<textobject><phrase>process types</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara><emphasis role="strong">#9 - Améliorer la robustesse de l&#8217;application grâce à des arrêts élégants et à des démarrages rapides</emphasis></simpara>
<simpara>Par "arrêt élégant", nous voulons surtout éviter le <literal>kill -9 &lt;pid&gt;</literal> ou tout autre arrêt brutal d&#8217;un processus qui nécessiterait une intervention urgente du superviseur.
De cette manière, les requêtes en cours peuvent se terminer au mieux, tandis que le démarrage rapide de nouveaux processus améliorera la balance d&#8217;un processus en cours d&#8217;extinction vers des processus tout frais.</simpara>
<simpara>L&#8217;intégration de ces mécanismes dès les premières étapes de développement limitera les perturbations et facilitera la prise en compte d&#8217;arrêts inopinés (problème matériel, redémarrage du système hôte, etc.).</simpara>
<simpara><emphasis role="strong">#10 - Conserver les différents environnements aussi similaires que possible, et limiter les divergences entre un environnement de développement et de production</emphasis></simpara>
<simpara>L&#8217;exemple donné est un développeur qui utilise macOS, NGinx et SQLite, tandis que l&#8217;environnement de production tourne sur une CentOS avec Apache2 et PostgreSQL.
Faire en sorte que tous les environnements soient les plus similaires possibles limite les divergences entre environnements, facilite les déploiements et limite la casse et la découverte de modules non compatibles dès les premières phases de développement.</simpara>
<simpara>Pour vous donner un exemple tout bête, SQLite utilise un <link xl:href="https://www.sqlite.org/datatype3.html">mécanisme de stockage dynamique</link>, associée à la valeur plutôt qu&#8217;au schéma, <emphasis>via</emphasis> un système d&#8217;affinités. Un autre moteur de base de données définira un schéma statique et rigide, où la valeur sera déterminée par son contenant.
Un champ <literal>URLField</literal> proposé par Django a une longeur maximale par défaut de <link xl:href="https://docs.djangoproject.com/en/3.1/ref/forms/fields/#django.forms.URLField">200 caractères</link>.
Si vous faites vos développements sous SQLite et que vous rencontrez une URL de plus de 200 caractères, votre développement sera passera parfaitement bien, mais plantera en production (ou en <emphasis>staging</emphasis>, si vous faites les choses un peu mieux) parce que les données seront tronquées&#8230;&#8203;</simpara>
<simpara>Conserver des environements similaires limite ce genre de désagréments.</simpara>
<simpara><emphasis role="strong">#11 - Gérer les journeaux d&#8217;évènements comme des flux</emphasis></simpara>
<simpara>Une application ne doit jamais se soucier de l&#8217;endroit où ses évènements seront écrits, mais simplement de les envoyer sur la sortie <literal>stdout</literal>.
De cette manière, que nous soyons en développement sur le poste d&#8217;un développeur avec une sortie console ou sur une machine de production avec un envoi vers une instance <link xl:href="https://www.graylog.org/">Greylog</link> ou <link xl:href="https://sentry.io/welcome/">Sentry</link>, le routage des journaux sera réalisé en fonction de sa nécessité et de sa criticité, et non pas parce que le développeur l&#8217;a spécifié en dur dans son code.
Cette phase est critique, dans la mesure où les journaux d&#8217;exécution sont la seule manière pour une application de communiquer son état vers l&#8217;extérieur: recevoir une erreur interne de serveur est une chose; pouvoir obtenir un minimum d&#8217;informations, voire un contexte de plantage complet en est une autre.</simpara>
<simpara><emphasis role="strong">#12 - Isoler les tâches administratives du reste de l&#8217;application</emphasis></simpara>
<simpara>Evitez qu&#8217;une migration ne puisse être démarrée depuis une URL de l&#8217;application, ou qu&#8217;un envoi massif de notifications ne soit accessible pour n&#8217;importe quel utilisateur: les tâches administratives ne doivent être accessibles qu&#8217;à un administrateur.
Les applications 12facteurs favorisent les langages qui mettent un environnement REPL (pour <emphasis>Read</emphasis>, <emphasis>Eval</emphasis>, <emphasis>Print</emphasis> et <emphasis>Loop</emphasis>) à disposition (au hasard: <link xl:href="https://pythonprogramminglanguage.com/repl/">Python</link> ou <link xl:href="https://kotlinlang.org/">Kotlin</link>), ce qui facilite les étapes de maintenance.</simpara>
</section>
<section xml:id="_concevoir_pour_lopérationnel">
<title>Concevoir pour l&#8217;opérationnel</title>
<simpara>Une application devient nettement plus maintenable dès lors que l&#8217;équipe de développement suit de près les différentes étapes de sa conception, de la demande jusqu&#8217;à son aboutissement en production. cite:[devops_handbook(293-294)]
Au fur et à mesure que le code est délibérément construit pour être maintenable, l&#8217;équipe gagne en rapidité, en qualité et en fiabilité de déploiement, ce qui facilite les tâches opérationnelles:</simpara>
<itemizedlist>
<listitem>
<simpara>Activation d&#8217;une télémétrie suffisante dans les applications et les environnements.</simpara>
</listitem>
<listitem>
<simpara>Conservation précise des dépendances nécessaires</simpara>
</listitem>
<listitem>
<simpara>Résilience des services et plantage élégant (i.e. <emphasis role="strong">sans finir sur un SEGFAULT avec l&#8217;OS dans les choux et un écran bleu</emphasis>)</simpara>
</listitem>
<listitem>
<simpara>Compatibilité entre les différentes versions (n+1, &#8230;&#8203;)</simpara>
</listitem>
<listitem>
<simpara>Gestion de l&#8217;espace de stockage associé à un environnement (pour éviter d&#8217;avoir un environnement de production qui fait 157 Tera-octets)</simpara>
</listitem>
<listitem>
<simpara>Activation de la recherche dans les logs</simpara>
</listitem>
<listitem>
<simpara>Traces des requêtes provenant des utilisateurs, indépendamment des services utilisés</simpara>
</listitem>
<listitem>
<simpara>Centralisation de la configuration (<emphasis role="strong">via</emphasis> ZooKeeper, par exemple)</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_robustesse_et_flexibilité">
<title>Robustesse et flexibilité</title>
<blockquote>
<simpara>Un code mal pensé entraîne nécessairement une perte d&#8217;énergie et de temps.
Il est plus simple de réfléchir, au moment de la conception du programme, à une architecture permettant une meilleure maintenabilité que de devoir corriger un code "sale" <emphasis>a posteriori</emphasis>.
C&#8217;est pour aider les développeurs à rester dans le droit chemin que les principes SOLID ont été énumérés. cite:[gnu_linux_mag_hs_104]</simpara>
</blockquote>
<simpara>Les principes SOLID, introduit par Robert C. Martin dans les années 2000 sont les suivants:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara><emphasis role="strong">SRP</emphasis> - Single responsibility principle - Principe de Responsabilité Unique</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">OCP</emphasis> - Open-closed principle</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">LSP</emphasis> - Liskov Substitution</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">ISP</emphasis> - Interface ségrégation principle</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">DIP</emphasis> - Dependency Inversion Principle</simpara>
</listitem>
</orderedlist>
<simpara>En plus de ces principes de développement, il faut ajouter des principes au niveau des composants, puis un niveau plus haut, au niveau, au niveau architectural :</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Reuse/release équivalence principle,</simpara>
</listitem>
<listitem>
<simpara>Common Closure Principle,</simpara>
</listitem>
<listitem>
<simpara>Common Reuse Principle.</simpara>
</listitem>
</orderedlist>
<section xml:id="_single_responsibility_principle">
<title>Single Responsibility Principle</title>
<simpara>Le principe de responsabilité unique conseille de disposer de concepts ou domaines d&#8217;activité qui ne s&#8217;occupent chacun que d&#8217;une et une seule chose.
Ceci rejoint (un peu) la <link xl:href="https://en.wikipedia.org/wiki/Unix_philosophy">Philosophie Unix</link>, documentée par Doug McIlroy et qui demande de "<emphasis>faire une seule chose, mais le faire bien</emphasis>" cite:[unix_philosophy].
Une classe ou un élément de programmtion ne doit donc pas avoir plus d&#8217;une raison de changer.</simpara>
<simpara>Il est également possible d&#8217;étendre ce principe en fonction d&#8217;acteurs:</simpara>
<blockquote>
<attribution>
Robert C. Martin
</attribution>
<simpara>A module should be responsible to one and only one actor. cite:[clean_architecture]</simpara>
</blockquote>
<simpara>Plutôt que de centraliser le maximum de code à un seul endroit ou dans une seule classe par convenance ou commodité <footnote><simpara>Aussi appelé <emphasis>God-Like object</emphasis></simpara></footnote>, le principe de responsabilité unique suggère que chaque classe soit responsable d&#8217;un et un seul concept.</simpara>
<simpara>Une manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez disposer d&#8217;une classe représentant des données de membres du personnel.
Ces données pourraient être demandées par trois acteurs, le CFO, le CTO et le COO.
Ceux-ci ont tous besoin de données et d&#8217;informations relatives à une même base de données centralisées, mais ont chacun besoin d&#8217;une représentation différente ou de traitements distincts. cite:[clean_architecture]</simpara>
<simpara>Nous sommes daccord quil sagit à chaque fois de données liées aux employés, mais elles vont un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de lacteur concerné et de la manière dont il souhaite disposer des données.
Dès que possible, identifiez les différents acteurs et demandeurs, en vue de prévoir les modifications qui pourraient être demandées par lun dentre eux.</simpara>
<simpara>Dans le cas d&#8217;un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres.</simpara>
<simpara>Vous trouverez ci-dessous une classe <literal>Document</literal>, dont chaque instance est représentée par trois propriétés: son titre, son contenu et sa date de publication.
Une méthode <literal>render</literal> permet également de proposer (très grossièrement) un type de sortie et un format de contenu: <literal>XML</literal> ou <literal>Markdown</literal>.</simpara>
<programlisting language="python" linenumbering="unnumbered">class Document:
def __init__(self, title, content, published_at):
self.title = title
self.content = content
self.published_at = published_at
def render(self, format_type):
if format_type == "XML":
return """&lt;?xml version = "1.0"?&gt;
&lt;document&gt;
&lt;title&gt;{}&lt;/title&gt;
&lt;content&gt;{}&lt;/content&gt;
&lt;publication_date&gt;{}&lt;/publication_date&gt;
&lt;/document&gt;""".format(
self.title,
self.content,
self.published_at.isoformat()
)
if format_type == "Markdown":
import markdown
return markdown.markdown(self.content)
raise ValueError("Format type '{}' is not known".format(format_type))</programlisting>
<simpara>Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, &#8230;&#8203;), il sera nécessaire de modifier la classe <literal>Document</literal>, ce qui n&#8217;est ni intuitif (<emphasis>ce n&#8217;est pas le document qui doit savoir dans quels formats il peut être envoyés</emphasis>), ni conseillé (<emphasis>lorsque nous aurons quinze formats différents à gérer, il sera nécessaire d&#8217;avoir autant de conditions dans cette méthode</emphasis>).</simpara>
<simpara>Une bonne pratique consiste à créer une nouvelle classe de rendu pour chaque type de format à gérer:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Document:
def __init__(self, title, content, published_at):
self.title = title
self.content = content
self.published_at = published_at
class DocumentRenderer:
def render(self, document):
if format_type == "XML":
return """&lt;?xml version = "1.0"?&gt;
&lt;document&gt;
&lt;title&gt;{}&lt;/title&gt;
&lt;content&gt;{}&lt;/content&gt;
&lt;publication_date&gt;{}&lt;/publication_date&gt;
&lt;/document&gt;""".format(
self.title,
self.content,
self.published_at.isoformat()
)
if format_type == "Markdown":
import markdown
return markdown.markdown(self.content)
raise ValueError("Format type '{}' is not known".format(format_type))</programlisting>
<simpara>A présent, lorsque nous devrons ajouter un nouveau format de prise en charge, nous irons modifier la classe <literal>DocumentRenderer</literal>, sans que la classe <literal>Document</literal> ne soit impactée.
En même temps, le jour où une instance de type <literal>Document</literal> sera liée à un champ <literal>author</literal>, rien ne dit que le rendu devra en tenir compte; nous modifierons donc notre classe pour y ajouter le nouveau champ sans que cela n&#8217;impacte nos différentes manières d&#8217;effectuer un rendu.</simpara>
<simpara>En prenant l&#8217;exemple d&#8217;une méthode qui communique avec une base de données, ce ne sera pas à cette méthode à gérer l&#8217;inscription d&#8217;une exception à un emplacement quelconque.
Cette action doit être prise en compte par une autre classe (ou un autre concept), qui s&#8217;occupera de définir elle-même l&#8217;emplacement où l&#8217;évènement sera enregistré, que ce soit dans une base de données, une instance Graylog ou un fichier.</simpara>
<simpara>Cette manière de structurer le code permet de centraliser la configuration d&#8217;un type d&#8217;évènement à un seul endroit, ce qui augmente ainsi la testabilité globale du projet.</simpara>
<simpara>Lorsque nous verrons les composants, le principe de responsabilité unique deviendra le CCP - Common Closure Principle.
Ensuite, lorsque nous verrons l&#8217;architecture de l&#8217;application, ce sera la définition des frontières (boundaries).</simpara>
</section>
<section xml:id="_open_closed">
<title>Open Closed</title>
<blockquote>
<simpara>For software systems to be easy to change, they must be designed to allow the behavior to change by adding new code instead of changing existing code.</simpara>
</blockquote>
<simpara>Lobjectif est de rendre le système facile à étendre, en évitant que limpact dune modification ne soit trop grand.</simpara>
<simpara>Les exemples parlent deux-mêmes: des données doivent être présentées dans une page web.
Et demain, ce seras dans un document PDF.
Et après demain, ce sera dans un tableur Excel.
La source de ces données restent la même (au travers dune couche de présentation), mais la mise en forme diffère à chaque fois.</simpara>
<simpara>Lapplication na pas à connaître les détails dimplémentation: elle doit juste permettre une forme dextension,
sans avoir à appliquer une modification (ou une grosse modification) sur son cœur.</simpara>
<simpara>Un des principes essentiels en programmation orientée objets concerne l&#8217;héritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons pour définir le comportement d&#8217;une instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise.
Pour l&#8217;exemple, on pourrait ainsi définir trois classes:</simpara>
<itemizedlist>
<listitem>
<simpara>Une classe <literal>Customer</literal>, pour laquelle la méthode <literal>GetDiscount</literal> ne renvoit rien;</simpara>
</listitem>
<listitem>
<simpara>Une classe <literal>SilverCustomer</literal>, pour laquelle la méthode revoit une réduction de 10%;</simpara>
</listitem>
<listitem>
<simpara>Une classe <literal>GoldCustomer</literal>, pour laquelle la même méthode renvoit une réduction de 20%.</simpara>
</listitem>
</itemizedlist>
<simpara>Si nous rencontrons un nouveau type de client, il suffit de créer une nouvelle sous-classe.
Cela évite d&#8217;avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d&#8217;une autre variable (ici, le type de client).</simpara>
<simpara>Nous passerions ainsi de:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Customer():
def __init__(self, customer_type: str):
self.customer_type = customer_type
def get_discount(customer: Customer) -&gt; int:
if customer.customer_type == "Silver":
return 10
elif customer.customer_type == "Gold":
return 20
return 0
&gt;&gt;&gt; jack = Customer("Silver")
&gt;&gt;&gt; jack.get_discount()
10</programlisting>
<simpara>A ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Customer():
def get_discount(self) -&gt; int:
return 0
class SilverCustomer(Customer):
def get_discount(self) -&gt; int:
return 10
class GoldCustomer(Customer):
def get_discount(self) -&gt; int:
return 20
&gt;&gt;&gt; jack = SilverCustomer()
&gt;&gt;&gt; jack.get_discount()
10</programlisting>
<simpara>En anglais, dans le texte : "<emphasis>Putting in simple words, the “Customer” class is now closed for any new modification but its open for extensions when new customer types are added to the project.</emphasis>".</simpara>
<simpara><emphasis role="strong">En résumé</emphasis>: nous fermons la classe <literal>Customer</literal> à toute modification, mais nous ouvrons la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types [héritant de <literal>Customer</literal>].</simpara>
<simpara>De cette manière, nous simplifions également la maintenance de la méthode <literal>get_discount</literal>, dans la mesure où elle dépend directement du type dans lequel elle est implémentée.</simpara>
<simpara>Nous pouvons également appliquer ceci à notre exemple sur les rendus de document, où le code suivant:</simpara>
<programlisting language="python" linenumbering="unnumbered">class DocumentRenderer:
def render(self, document):
if format_type == "XML":
return """&lt;?xml version = "1.0"?&gt;
&lt;document&gt;
&lt;title&gt;{}&lt;/title&gt;
&lt;content&gt;{}&lt;/content&gt;
&lt;publication_date&gt;{}&lt;/publication_date&gt;
&lt;/document&gt;""".format(
document.title,
document.content,
document.published_at.isoformat()
)
if format_type == "Markdown":
import markdown
return markdown.markdown(document.content)
raise ValueError("Format type '{}' is not known".format(format_type))</programlisting>
<simpara>devient le suivant:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Renderer:
def render(self, document):
raise NotImplementedError
class XmlRenderer(Renderer):
def render(self, document)
return """&lt;?xml version = "1.0"?&gt;
&lt;document&gt;
&lt;title&gt;{}&lt;/title&gt;
&lt;content&gt;{}&lt;/content&gt;
&lt;publication_date&gt;{}&lt;/publication_date&gt;
&lt;/document&gt;""".format(
document.title,
document.content,
document.published_at.isoformat()
)
class MarkdownRenderer(Renderer):
def render(self, document):
import markdown
return markdown.markdown(document.content)</programlisting>
<simpara>Lorsque nous ajouterons notre nouveau type de rendu, nous ajouterons simplement une nouvelle classe de rendu qui héritera de <literal>Renderer</literal>.</simpara>
<simpara>Ce point sera très utile lorsque nous aborderons les <link xl:href="https://docs.djangoproject.com/en/stable/topics/db/models/#proxy-models">modèles proxy</link>.</simpara>
</section>
<section xml:id="_liskov_substitution">
<title>Liskov Substitution</title>
<note>
<simpara>Dans Clean Architecture, ce chapitre ci (le 9) est sans doute celui qui est le moins complet.
Je suis daccord avec les exemples donnés, dans la mesure où la définition concrète dune classe doit dépendre dune interface correctement définie (et que donc, faire hériter un carré dun rectangle, nest pas adéquat dans le mesure où cela induit lutilisateur en erreur), mais il y est aussi question de la définition d&#8217;un style architectural pour une interface REST, mais sans donner de solution&#8230;&#8203;</simpara>
</note>
<simpara>Le principe de substitution fait qu&#8217;une classe héritant d&#8217;une autre classe doit se comporter de la même manière que cette dernière.
Il n&#8217;est pas question que la sous-classe n&#8217;implémente pas certaines méthodes, alors que celles-ci sont disponibles sa classe parente.</simpara>
<blockquote>
<simpara>[&#8230;&#8203;] if S is a subtype of T, then objects of type T in a computer program may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T), without altering any of the desirable properties of that program (correctness, task performed, etc.). (Source: <link xl:href="http://en.wikipedia.org/wiki/Liskov_substitution_principle">Wikipédia</link>).</simpara>
</blockquote>
<blockquote>
<simpara>Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S, where S is a subtype of T. (Source: <link xl:href="http://en.wikipedia.org/wiki/Liskov_substitution_principle">Wikipédia aussi</link>)</simpara>
</blockquote>
<simpara>Ce n&#8217;est donc pas parce qu&#8217;une classe <emphasis role="strong">a besoin d&#8217;une méthode définie dans une autre classe</emphasis> qu&#8217;elle doit forcément en hériter.
Cela bousillerait le principe de substitution, dans la mesure où une instance de cette classe pourra toujours être considérée comme étant du type de son parent.</simpara>
<simpara>Petit exemple pratique: si nous définissons une méthode <literal>walk</literal> et une méthode <literal>eat</literal> sur une classe <literal>Duck</literal>, et qu&#8217;une réflexion avancée (et sans doute un peu alcoolisée) nous dit que "<emphasis>Puisqu&#8217;un <literal>Lion</literal> marche aussi, faisons le hériter de notre classe `Canard`"</emphasis>, nous allons nous retrouver avec ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Duck:
def walk(self):
print("Kwak")
def eat(self, thing):
if thing in ("plant", "insect", "seed", "seaweed", "fish"):
return "Yummy!"
raise IndigestionError("Arrrh")
class Lion(Duck):
def walk(self):
print("Roaaar!")</programlisting>
<simpara>Le principe de substitution de Liskov suggère qu&#8217;une classe doit toujours pouvoir être considérée comme une instance de sa classe parent, et <emphasis role="strong">doit pouvoir s&#8217;y substituer</emphasis>.
Dans notre exemple, cela signifie que nous pourrons tout à fait accepter qu&#8217;un lion se comporte comme un canard et adore manger des plantes, insectes, graines, algues et du poisson. Miam !
Nous vous laissons tester la structure ci-dessus en glissant une antilope dans la boite à goûter du lion, ce qui nous donnera quelques trucs bizarres (et un lion atteint de botulisme).</simpara>
<simpara>Pour revenir à nos exemples de rendus de documents, nous aurions pu faire hériter notre <literal>MarkdownRenderer</literal> de la classe <literal>XmlRenderer</literal>:</simpara>
<programlisting language="python" linenumbering="unnumbered">class XmlRenderer:
def render(self, document)
return """&lt;?xml version = "1.0"?&gt;
&lt;document&gt;
&lt;title&gt;{}&lt;/title&gt;
&lt;content&gt;{}&lt;/content&gt;
&lt;publication_date&gt;{}&lt;/publication_date&gt;
&lt;/document&gt;""".format(
document.title,
document.content,
document.published_at.isoformat()
)
class MarkdownRenderer(XmlRenderer):
def render(self, document):
import markdown
return markdown.markdown(document.content)</programlisting>
<simpara>Mais lorsque nous ajouterons une fonction d&#8217;entête, notre rendu en Markdown héritera irrémédiablement de cette même méthode:</simpara>
<programlisting language="python" linenumbering="unnumbered">class XmlRenderer:
def header(self):
return """&lt;?xml version = "1.0"?&gt;"""
def render(self, document)
return """{}
&lt;document&gt;
&lt;title&gt;{}&lt;/title&gt;
&lt;content&gt;{}&lt;/content&gt;
&lt;publication_date&gt;{}&lt;/publication_date&gt;
&lt;/document&gt;""".format(
self.header(),
document.title,
document.content,
document.published_at.isoformat()
)
class MarkdownRenderer(XmlRenderer):
def render(self, document):
import markdown
return markdown.markdown(document.content)</programlisting>
<simpara>A nouveau, lorsque nous invoquerons la méthode <literal>header()</literal> sur une instance de type <literal>MarkdownRenderer</literal>, nous obtiendrons un bloc de déclaration XML (<literal>&lt;?xml version = "1.0"?&gt;</literal>) pour un fichier Markdown.</simpara>
</section>
<section xml:id="_interface_segregation_principle">
<title>Interface Segregation Principle</title>
<simpara>Le principe de ségrégation d&#8217;interface suggère de limiter la nécessité de recompiler un module, en nexposant que les opérations nécessaires à lexécution dune classe.
Ceci évite davoir à redéployer lensemble dune application.</simpara>
<blockquote>
<simpara>The lesson here is that depending on something that carries baggage that you dont need can cause you troubles that you didnt except.</simpara>
</blockquote>
<simpara>Ce principe stipule qu&#8217;un client ne doit pas dépendre d&#8217;une méthode dont il n&#8217;a pas besoin.
Plus simplement, plutôt que de dépendre d&#8217;une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d&#8217;exploser cette interface en plusieurs (plus petites) interfaces.
Ceci permet aux différents consommateurs de n&#8217;utiliser qu&#8217;un sous-ensemble précis d&#8217;interfaces, répondant chacune à un besoin précis.</simpara>
<simpara>GNU/Linux Magazine cite:[gnu_linux_mag_hs_104(37-42)] propose un exemple d&#8217;interface permettant d&#8217;implémenter une imprimante:</simpara>
<programlisting language="java" linenumbering="unnumbered">interface IPrinter
{
public abstract void printPage();
public abstract void scanPage();
public abstract void faxPage();
}
public class Printer
{
protected string name;
public Printer(string name)
{
this.name = name;
}
}</programlisting>
<simpara>L&#8217;implémentation d&#8217;une imprimante multifonction aura tout son sens:</simpara>
<programlisting language="java" linenumbering="unnumbered">public class AllInOnePrinter implements Printer extends IPrinter
{
public AllInOnePrinter(string name)
{
super(name);
}
public void printPage()
{
System.out.println(this.name + ": Impression");
}
public void scanPage()
{
System.out.println(this.name + ": Scan");
}
public void faxPage()
{
System.out.println(this.name + ": Fax");
}
}</programlisting>
<simpara>Tandis que l&#8217;implémentation d&#8217;une imprimante premier-prix ne servira pas à grand chose:</simpara>
<programlisting language="java" linenumbering="unnumbered">public class FirstPricePrinter implements Printer extends IPrinter
{
public FirstPricePrinter(string name)
{
super(name);
}
public void printPage()
{
System.out.println(this.name + ": Impression");
}
public void scanPage()
{
System.out.println(this.name + ": Fonctionnalité absente");
}
public void faxPage()
{
System.out.println(this.name + ": Fonctionnalité absente");
}
}</programlisting>
<simpara>L&#8217;objectif est donc de découpler ces différentes fonctionnalités en plusieurs interfaces bien spécifiques, implémentant chacune une opération isolée:</simpara>
<programlisting language="java" linenumbering="unnumbered">interface IPrinterPrinter
{
public abstract void printPage();
}
interface IPrinterScanner
{
public abstract void scanPage();
}
interface IPrinterFax
{
public abstract void faxPage();
}</programlisting>
<simpara>Cette réflexion s&#8217;applique finalement à n&#8217;importe quel composant: votre système d&#8217;exploitation, les librairies et dépendances tierces, les variables déclarées, &#8230;&#8203;
Quel que soit le composant que l&#8217;on utilise ou analyse, il est plus qu&#8217;intéressant de se limiter uniquement à ce dont nous avons besoin plutôt que</simpara>
<simpara>En Python, ce comportement est inféré lors de lexécution, et donc pas vraiment dapplication pour notre contexte d&#8217;étude: de manière plus générale, les langages dynamiques sont plus flexibles et moins couplés que les langages statiquement typés, pour lesquels l&#8217;application de ce principe-ci permettrait de mettre à jour une DLL ou un JAR sans que cela nait dimpact sur le reste de lapplication.</simpara>
<simpara>Il est ainsi possible de trouver quelques horreurs, dans tous les langages:</simpara>
<programlisting language="javascript" linenumbering="unnumbered">/*!
* is-odd &lt;https://github.com/jonschlinkert/is-odd&gt;
*
* Copyright (c) 2015-2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
const isNumber = require('is-number');
module.exports = function isOdd(value) {
const n = Math.abs(value);
if (!isNumber(n)) {
throw new TypeError('expected a number');
}
if (!Number.isInteger(n)) {
throw new Error('expected an integer');
}
if (!Number.isSafeInteger(n)) {
throw new Error('value exceeds maximum safe integer');
}
return (n % 2) === 1;
};</programlisting>
<simpara>Voire, son opposé, qui dépend évidemment du premier:</simpara>
<programlisting language="javascript" linenumbering="unnumbered">/*!
* is-even &lt;https://github.com/jonschlinkert/is-even&gt;
*
* Copyright (c) 2015, 2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
var isOdd = require('is-odd');
module.exports = function isEven(i) {
return !isOdd(i);
};</programlisting>
<simpara>Il ne s&#8217;agit que d&#8217;un simple exemple, mais qui tend à une seule chose: gardez les choses simples (et, éventuellement, stupides) <indexterm>
<primary>kiss</primary>
</indexterm>.
Dans l&#8217;exemple ci-dessus, l&#8217;utilisation du module <literal>is-odd</literal> requière déjà deux dépendances: <literal>is-even</literal> et <literal>is-number</literal>.
Imaginez la suite.</simpara>
</section>
<section xml:id="_dependency_inversion_principle">
<title>Dependency inversion Principle</title>
<simpara>Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau.
L&#8217;inversion de dépendances stipule que c&#8217;est le composant de haut-niveau qui possède la définition de l&#8217;interface dont il a besoin, et le composant de bas-niveau qui l&#8217;implémente.
Lobjectif est que les interfaces soient les plus stables possibles, afin de réduire au maximum les modifications qui pourraient y être appliquées.
De cette manière, toute modification fonctionnelle pourra être directement appliquée sur le composant de bas-niveau, sans que l&#8217;interface ne soit impactée.</simpara>
<blockquote>
<simpara>The dependency inversion principle tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.</simpara>
</blockquote>
<simpara>cite:[clean_architecture]</simpara>
<simpara>L&#8217;injection de dépendances est un patron de programmation qui suit le principe d&#8217;inversion de dépendances.</simpara>
<simpara>Django est bourré de ce principe, que ce soit pour les <emphasis>middlewares</emphasis> ou pour les connexions aux bases de données.
Lorsque nous écrivons ceci dans notre fichier de configuration,</simpara>
<programlisting language="python" linenumbering="unnumbered"># [snip]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# [snip]</programlisting>
<simpara>Django ira simplement récupérer chacun de ces middlewares, qui répondent chacun à une <link xl:href="https://docs.djangoproject.com/en/4.0/topics/http/middleware/#writing-your-own-middleware">interface clairement définie</link>, dans l&#8217;ordre.
Il n&#8217;y a donc pas de magie; c&#8217;est le développeur qui va simplement brancher ou câbler des fonctionnalités au niveau du framework, en les déclarant au bon endroit.
Pour créer un nouveau <emphasis>middleware</emphasis>, il suffira d&#8217;implémenter le code suivant et de l&#8217;ajouter dans la configuration de l&#8217;application:</simpara>
<programlisting language="python" linenumbering="unnumbered">def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
return middleware</programlisting>
<simpara>Dans d&#8217;autres projets écrits en Python, ce type de mécanisme peut être implémenté relativement facilement en utilisant les modules <link xl:href="https://docs.python.org/3/library/importlib.html">importlib</link> et la fonction <literal>getattr</literal>.</simpara>
<simpara>Un autre exemple concerne les bases de données: pour garder un maximum de flexibilité, Django ajoute une couche d&#8217;abstraction en permettant de spécifier le moteur de base de données que vous souhaiteriez utiliser, qu&#8217;il s&#8217;agisse d&#8217;SQLite, MSSQL, Oracle, PostgreSQL ou MySQL/MariaDB <footnote><simpara><link xl:href="http://howfuckedismydatabase.com/">http://howfuckedismydatabase.com/</link></simpara></footnote>.</simpara>
<blockquote>
<simpara>The database is really nothing more than a big bucket of bits where we store our data on a long term basis.</simpara>
</blockquote>
<simpara>cite:[clean_architecture(281)]</simpara>
<simpara>Dun point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, sil sagit dun disque magnétique, de ram, &#8230;&#8203; en fait, on ne devrait même pas savoir sil y a un disque du tout.
Et Django le fait très bien pour nous.</simpara>
<simpara>En termes architecturaux, ce principe autorise une définition des frontières, et en permettant une séparation claire en inversant le flux de dépendances et en faisant en sorte que les règles métiers n&#8217;aient aucune connaissance des interfaces graphiques qui les exploitent ou des moteurs de bases de données qui les stockent.
Ceci autorise une forme d&#8217;immunité entre les composants.</simpara>
</section>
<section xml:id="_sources">
<title>Sources</title>
<itemizedlist>
<listitem>
<simpara><link xl:href="http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp">Understanding SOLID principles on CodeProject</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/">Dependency Injection is NOT the same as dependency inversion</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="http://en.wikipedia.org/wiki/Dependency_injection">Injection de dépendances</link></simpara>
</listitem>
</itemizedlist>
</section>
</section>
<section xml:id="_au_niveau_des_composants_2">
<title>au niveau des composants</title>
<simpara>De la même manière que pour les principes définis ci-dessus,
Mais toujours en faisant attention quune fois que les frontières sont implémentés, elles sont coûteuses à maintenir.
Cependant, il ne sagit pas une décision à réaliser une seule fois, puisque cela peut être réévalué.</simpara>
<simpara>Et de la même manière que nous devons délayer au maximum les choix architecturaux et techniques,</simpara>
<blockquote>
<simpara>but this is not a one time decision. You dont simply decide at the start of a project which boundaries to implémentent and which to ignore. Rather, you watch. You pay attention as the system evolves. You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries dont exist.
at that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring.</simpara>
</blockquote>
<simpara>En gros, il faut projeter sur la capacité à sadapter en minimisant la maintenance.
Le problème est quelle ne permettait aucune adaptation, et quà la première demande, larchitecture se plante complètement sans aucune malléabilité.</simpara>
<section xml:id="_reuserelease_equivalence_principle">
<title>Reuse/release equivalence principle</title>
<screen>Classes and modules that are grouped together into a component should be releasable together
-- (Chapitre 13, Component Cohesion, page 105)</screen>
</section>
<section xml:id="_ccp">
<title>CCP</title>
<simpara>(= léquivalent du SRP, mais pour les composants)</simpara>
<blockquote>
<simpara>If two classes are so tightly bound, either physically or conceptually, that they always change together, then they belong in the same component</simpara>
</blockquote>
<simpara>Il y a peut-être aussi un lien à faire avec « Your code as a crime scene » 🤟</simpara>
<literallayout class="monospaced">La définition exacte devient celle-ci: « gather together those things that change at the same times and for the same reasons. Separate those things that change at different times or for different reasons ».</literallayout>
<literallayout class="monospaced">==== CRP</literallayout>
<literallayout class="monospaced"> … que lon résumera ainsi: « dont depend on things you dont need » 😘
Au niveau des composants, au niveau architectural, mais également à dautres niveaux.</literallayout>
</section>
<section xml:id="_sdp">
<title>SDP</title>
<simpara>(Stable dependency principle) qui définit une formule de stabilité pour les composants, en fonction de sa faculté à être modifié et des composants qui dépendent de lui: au plus un composant est nécessaire, au plus il sera stable (dans la mesure où il lui sera difficile de changer). En C++, cela correspond aux mots clés #include.
Pour faciliter cette stabilité, il convient de passer par des interfaces (donc, rarement modifiées, par définition).</simpara>
<simpara>En Python, ce ratio pourrait être calculé au travers des import, via les AST.</simpara>
</section>
<section xml:id="_sap">
<title>SAP</title>
<simpara>(= Stable abstraction principle) pour la définition des politiques de haut niveau vs les composants plus concrets.
SAP est juste une modélisation du OCP pour les composants: nous plaçons ceux qui ne changent pas ou pratiquement pas le plus haut possible dans lorganigramme (ou le diagramme), et ceux qui changent souvent plus bas, dans le sens de stabilité du flux. Les composants les plus bas sont considérés comme volatiles</simpara>
</section>
</section>
</chapter>
<chapter xml:id="_architecture">
<title>Architecture</title>
<blockquote>
<attribution>
Brian Foote and Joseph Yoder
</attribution>
<simpara>If you think good architecture is expensive, try bad architecture</simpara>
</blockquote>
<simpara>Au delà des principes dont il est question plus haut, cest dans les ressources proposées et les cas démontrés que lon comprend leur intérêt: plus que de la définition dune architecture adéquate, cest surtout dans la facilité de maintenance dune application que ces principes sidentifient.</simpara>
<simpara>Une bonne architecture va rendre le système facile à lire, facile à développer, facile à maintenir et facile à déployer.
L&#8217;objectif ultime étant de minimiser le coût de maintenance et de maximiser la productivité des développeurs.
Un des autres objectifs d&#8217;une bonne architecture consiste également à se garder le plus doptions possibles,
et à se concentrer sur les détails (le type de base de données, la conception concrète, &#8230;&#8203;),
le plus tard possible, tout en conservant la politique principale en ligne de mire.
Cela permet de délayer les choix techniques à « plus tard », ce qui permet également de concrétiser ces choix
en ayant le plus dinformations possibles cite:[clean_architecture(137-141)]</simpara>
<simpara>Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires à faire évoluer lapplication: ne pas investir dès quon le peut va juste lentement remplir la case de la dette technique.</simpara>
<simpara>Une architecture ouverte et pouvant être étendue na dintérêt que si le développement est suivi et que les gestionnaires (et architectes) sengagent à économiser du temps et de la qualité lorsque des changements seront demandés pour lévolution du projet.</simpara>
<section xml:id="_politiques_et_règles_métiers">
<title>Politiques et règles métiers</title>
<simpara>TODO: Un p&#8217;tit bout à ajouter sur les méthodes de conception ;)</simpara>
</section>
<section xml:id="_considération_sur_les_frameworks">
<title>Considération sur les frameworks</title>
<blockquote>
<attribution>
Robert C. Martin
<citetitle>Clean Architecture</citetitle>
</attribution>
<simpara>Frameworks are tools to be used, not architectures to be conformed to.
Your architecture should tell readers about the system, not about the frameworks you used in your system.
If you are building a health care system, then when new programmers look at the source repository,
their first impression should be, « oh, this is a health care system ».
Those new programmers should be able to learn all the use cases of the system,
yet still not know how the system is delivered.</simpara>
</blockquote>
<simpara>Le point soulevé ci-dessous est qu&#8217;un framework n&#8217;est qu&#8217;un outil, et pas une obligation de structuration.
L&#8217;idée est que le framework doit se conformer à la définition de l&#8217;application, et non l&#8217;inverse.
Dans le cadre de l&#8217;utilisation de Django, c&#8217;est un point critique à prendre en considération: une fois que vous aurez fait
ce choix, vous aurez extrêmement difficile à faire machine arrière:</simpara>
<itemizedlist>
<listitem>
<simpara>Votre modèle métier sera largement couplé avec le type de base de données (relationnelle, indépendamment</simpara>
</listitem>
<listitem>
<simpara>Votre couche de présentation sera surtout disponible au travers d&#8217;un navigateur</simpara>
</listitem>
<listitem>
<simpara>Les droits d&#8217;accès et permissions seront en grosse partie gérés par le frameworks</simpara>
</listitem>
<listitem>
<simpara>La sécurité dépendra de votre habilité à suivre les versions</simpara>
</listitem>
<listitem>
<simpara>Et les fonctionnalités complémentaires (que vous n&#8217;aurez pas voulu/eu le temps de développer) dépendront
de la bonne volonté de la communauté</simpara>
</listitem>
</itemizedlist>
<simpara>Le point à comprendre ici n&#8217;est pas que "Django, c&#8217;est mal", mais qu&#8217;une fois que vous aurez défini la politique,
les règles métiers, les données critiques et entités, et que vous aurez fait le choix de développer en âme et conscience
votre nouvelle création en utilisant Django, vous serez bon gré mal gré, contraint de continuer avec.
Cette décision ne sera pas irrévocable, mais difficile à contourner.</simpara>
<blockquote>
<simpara>At some point in their history most DevOps organizations were hobbled by tightly-coupled, monolithic architectures that while extremely successfull at helping them achieve product/market fit - put them at risk of organizational failure once they had to operate at scale (e.g. eBay&#8217;s monolithic C++ application in 2001, Amazon&#8217;s monolithic OBIDOS application in 2001, Twitter&#8217;s monolithic Rails front-end in 2009, and LinkedIn&#8217;s monolithic Leo application in 2011).
In each of these cases, they were able to re-architect their systems and set the stage not only to survice, but also to thrise and win in the marketplace</simpara>
</blockquote>
<simpara>cite:[devops_handbook(182)]</simpara>
<simpara>Ceci dit, Django compense ses contraintes en proposant énormément de flexibilité et de fonctionnalités
<emphasis role="strong">out-of-the-box</emphasis>, c&#8217;est-à-dire que vous pourrez sans doute avancer vite et bien jusqu&#8217;à un point de rupture,
puis revoir la conception et réinvestir à ce moment-là, mais en toute connaissance de cause.</simpara>
<blockquote>
<attribution>
Robert C. Martin
<citetitle>Clean Architecture</citetitle>
</attribution>
<simpara>When any of the external parts of the system become obsolete, such as the database, or the web framework,
you can replace those obsolete elements with a minimum of fuss.</simpara>
</blockquote>
<simpara>Avec Django, la difficulté à se passer du framework va consister à basculer vers « autre chose » et a remplacer
chacune des tentacules qui aura pousser partout dans lapplication.</simpara>
<note>
<simpara>A noter que les services et les « architectures orientées services » ne sont jamais quune définition
dimplémentation des frontières, dans la mesure où un service nest jamais quune fonction appelée au travers d&#8217;un protocole
(rest, soap, &#8230;&#8203;).
Une application monolotihique sera tout aussi fonctionnelle quune application découpée en microservices.
(Services: great and small, page 243).</simpara>
</note>
</section>
<section xml:id="_un_point_sur_linversion_de_dépendances">
<title>Un point sur l&#8217;inversion de dépendances</title>
<simpara>Dans la partie SOLID, nous avons évoqué plusieurs principes de développement.
Django est un framework qui évolue, et qui a pu présenter certains problèmes liés à l&#8217;un de ces principes.</simpara>
<simpara>Les link:release notes[<link xl:href="https://docs.djangoproject.com/en/2.0/releases/2.0/">https://docs.djangoproject.com/en/2.0/releases/2.0/</link>] de Django 2.0 date de décembre 2017; parmi ces notes,
l&#8217;une d&#8217;elles cite l&#8217;abandon du support d&#8217;link:Oracle 11.2[<link xl:href="https://docs.djangoproject.com/en/2.0/releases/2.0/#dropped-support-for-oracle-11-2">https://docs.djangoproject.com/en/2.0/releases/2.0/#dropped-support-for-oracle-11-2</link>].
En substance, cela signifie que le framework se chargeait lui-même de construire certaines parties de requêtes,
qui deviennent non fonctionnelles dès lors que l&#8217;on met le framework ou le moteur de base de données à jour.
Réécrit, cela signifie que:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Si vos données sont stockées dans un moteur géré par Oracle 11.2, vous serez limité à une version 1.11 de Django</simpara>
</listitem>
<listitem>
<simpara>Tandis que si votre moteur est géré par une version ultérieure, le framework pourra être mis à jour.</simpara>
</listitem>
</orderedlist>
<simpara>Nous sommes dans un cas concret d&#8217;inversion de dépendances ratée: le framework (et encore moins vos politiques et règles métiers)
ne devraient pas avoir connaissance du moteur de base de données.
Pire, vos politiques et données métiers ne devraient pas avoir connaissance <emphasis role="strong">de la version</emphasis> du moteur de base de données.</simpara>
<simpara>En conclusion, le choix d&#8217;une version d&#8217;un moteur technique (<emphasis role="strong">la base de données</emphasis>) a une incidence directe sur les fonctionnalités
mises à disposition par votre application, ce qui va à l&#8217;encontre des 12 facteurs (et des principes de développement).</simpara>
<simpara>Ce point sera rediscuté par la suite, notamment au niveau de l&#8217;épinglage des versions, de la reproduction des environnements
et de l&#8217;interdépendance entre des choix techniques et fonctionnels.</simpara>
</section>
</chapter>
<chapter xml:id="_tests_et_intégration">
<title>Tests et intégration</title>
<blockquote>
<attribution>
Robert C. Martin
<citetitle>Clean Architecture</citetitle>
</attribution>
<simpara>Tests are part of the system.
You can think of tests as the outermost circle in the architecture.
Nothing within in the system depends on the tests, and the tests always depend inward on the components of the system ».</simpara>
</blockquote>
</chapter>
<chapter xml:id="_le_langage_python">
<title>Le langage Python</title>
<simpara>Le langage <link xl:href="https://www.python.org/">Python</link> est un <link xl:href="https://docs.python.org/3/faq/general.html#what-is-python">langage de programmation</link> interprété, interactif, amusant, orienté objet (souvent), fonctionnel (parfois), open source, multi-plateformes, flexible, facile à apprendre et difficile à maîtriser.</simpara>
<figure>
<title><link xl:href="https://xkcd.com/353/">https://xkcd.com/353/</link></title>
<mediaobject>
<imageobject>
<imagedata fileref="images/xkcd-353-python.png"/>
</imageobject>
<textobject><phrase>xkcd 353 python</phrase></textobject>
</mediaobject>
</figure>
<simpara>A première vue, et suivants les autres langages que vous connaitriez ou auriez déjà abordé, certains concepts restent difficiles à aborder: l&#8217;indentation définit l&#8217;étendue d&#8217;un bloc (classe, fonction, méthode, boucle, condition, &#8230;&#8203;), il n&#8217;y a pas de typage fort des variables et le compilateur n&#8217;est pas là pour assurer le filet de sécurité avant la mise en production (puisqu&#8217;il n&#8217;y a pas de compilateur 😛).
Et malgré ces quelques points, Python reste un langage généraliste accessible et "bon partout", et de pouvoir se reposer sur un écosystème stable et fonctionnel.</simpara>
<simpara>Il fonctionne avec un système d&#8217;améliorations basées sur des propositions: les PEP, ou "<emphasis role="strong">Python Enhancement Proposal</emphasis>".
Chacune d&#8217;entre elles doit être approuvée par le <link xl:href="http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life">Benevolent Dictator For Life</link>.</simpara>
<note>
<simpara>Le langage Python utilise un typage dynamique appelé <link xl:href="https://fr.wikipedia.org/wiki/Duck_typing"><emphasis role="strong">duck typing</emphasis></link>: "<emphasis>When I see a bird that quacks like a duck, walks like a duck, has feathers and webbed feet and associates with ducks — Im certainly going to assume that he is a duck</emphasis>"
Source: <link xl:href="http://en.wikipedia.org/wiki/Duck_test">Wikipedia</link>.</simpara>
</note>
<simpara>Les morceaux de code que vous trouverez ci-dessous seront développés pour Python3.9+ et Django 3.2+.
Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure.</simpara>
<section xml:id="_eléments_de_langage">
<title>Eléments de langage</title>
<simpara>En fonction de votre niveau d&#8217;apprentissage du langage, plusieurs ressources pourraient vous aider:</simpara>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">Pour les débutants</emphasis>, <link xl:href="https://automatetheboringstuff.com/">Automate the Boring Stuff with Python</link> cite:[boring_stuff], aka. <emphasis>Practical Programming for Total Beginners</emphasis></simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Pour un (gros) niveau au dessus</emphasis> et pour un état de l&#8217;art du langage, nous ne pouvons que vous recommander le livre Expert Python Programming cite:[expert_python], qui aborde énormément d&#8217;aspects du langage en détails (mais pas toujours en profondeur): les différents types d&#8217;interpréteurs, les éléments de langage avancés, différents outils de productivité, métaprogrammation, optimisation de code, programmation orientée évènements, multithreading et concurrence, tests, &#8230;&#8203;
A ce jour, c&#8217;est le concentré de sujets liés au langage le plus intéressant qui ait pu arriver entre nos mains.</simpara>
</listitem>
</itemizedlist>
<simpara>En parallèle, si vous avez besoin d&#8217;un aide-mémoire ou d&#8217;une liste exhaustive des types et structures de données du langage, référez-vous au lien suivant: <link xl:href="https://gto76.github.io/python-cheatsheet/">Python Cheat Sheet</link>.</simpara>
<section xml:id="_protocoles_de_langage">
<title>Protocoles de langage</title>
<simpara><indexterm>
<primary>dunder</primary>
</indexterm></simpara>
<simpara>Le modèle de données du langage spécifie un ensemble de méthodes qui peuvent être surchargées.
Ces méthodes suivent une convention de nommage et leur nom est toujours encadré par un double tiret souligné; d&#8217;où leur nom de "<emphasis>dunder methods</emphasis>" ou "<emphasis>double-underscore methods</emphasis>".
La méthode la plus couramment utilisée est la méthode <literal><emphasis>init</emphasis>()</literal>, qui permet de surcharger l&#8217;initialisation d&#8217;une instance de classe.</simpara>
<programlisting language="python" linenumbering="unnumbered">class CustomUserClass:
def __init__(self, initiatization_argument):
...</programlisting>
<simpara>cite:[expert_python(142-144)]</simpara>
<simpara>Ces méthodes, utilisées seules ou selon des combinaisons spécifiques, constituent les <emphasis>protocoles de langage</emphasis>.
Une liste complètement des <emphasis>dunder methods</emphasis> peut être trouvée dans la section <literal>Data Model</literal> de <link xl:href="https://docs.python.org/3/reference/datamodel.html">la documentation du langage Python</link>.</simpara>
<simpara>All operators are also exposed as ordinary functions in the operators module.
The documentation of that module gives a good overview of Python operators.
It can be found at <link xl:href="https://docs.python.org/3.9/library/operator.html">https://docs.python.org/3.9/library/operator.html</link></simpara>
<simpara>If we say that an object implements a specific language protocol, it means that it is compatible with a specific part of the Python language syntax.</simpara>
<simpara>The following is a table of the most common protocols within the Python language.</simpara>
<simpara>Protocol nameMethodsDescriptionCallable protocol<emphasis>call</emphasis>()Allows objects to be called with parentheses:instance()Descriptor protocols<emphasis>set</emphasis>(), <emphasis>get</emphasis>(), and <emphasis>del</emphasis>()Allows us to manipulate the attribute access pattern of classes (see the Descriptors section)Container protocol<emphasis>contains</emphasis>()Allows us to test whether or not an object contains some value using the in keyword:value in instance</simpara>
<simpara>Python in Comparison with Other LanguagesIterable protocol<emphasis>iter</emphasis>()Allows objects to be iterated using the forkeyword:for value in instance: &#8230;&#8203;Sequence protocol<emphasis>getitem</emphasis>(),<emphasis>len</emphasis>()Allows objects to be indexed with square bracket syntax and queried for length using a built-in function:item = instance[index]length = len(instance)Each operator available in Python has its own protocol and operator overloading happens by implementing the dunder methods of that protocol. Python provides over 50 overloadable operators that can be divided into five main groups:• Arithmetic operators • In-place assignment operators• Comparison operators• Identity operators• Bitwise operatorsThat&#8217;s a lot of protocols so we won&#8217;t discuss all of them here. We will instead take a look at a practical example that will allow you to better understand how to implement operator overloading on your own</simpara>
<simpara>The <literal><emphasis>add</emphasis>()</literal> method is responsible for overloading the <literal>+</literal> (plus sign) operator and here it allows us to add
two matrices together.
Only matrices of the same dimensions can be added together.
This is a fairly simple operation that involves adding all matrix elements one by one to form a new matrix.</simpara>
<simpara>The <literal><emphasis>sub</emphasis>()</literal> method is responsible for overloading the <literal></literal> (minus sign) operator that will be responsible for matrix subtraction.
To subtract two matrices, we use a similar technique as in the operator:</simpara>
<programlisting language="python" linenumbering="unnumbered">def __sub__(self, other):
if (len(self.rows) != len(other.rows) or len(self.rows[0]) != len(other.rows[0])):
raise ValueError("Matrix dimensions don't match")
return Matrix([[a - b for a, b in zip(a_row, b_row)] for a_row, b_row in zip(self.rows, other.rows) ])</programlisting>
<simpara>And the following is the last method we add to our class:</simpara>
<programlisting language="python" linenumbering="unnumbered">def __mul__(self, other):
if not isinstance(other, Matrix):
raise TypeError(f"Don't know how to multiply {type(other)} with Matrix")
if len(self.rows[0]) != len(other.rows):
raise ValueError("Matrix dimensions don't match")
rows = [[0 for _ in other.rows[0]] for _ in self.rows]
for i in range(len (self.rows)):
for j in range(len (other.rows[0])):
for k in range(len (other.rows)):
rows[i][j] += self.rows[i][k] * other.rows[k][j]
return Matrix(rows)</programlisting>
<simpara>The last overloaded operator is the most complex one.
This is the <literal>*</literal> operator, which is implemented through the <literal><emphasis>mul</emphasis>()</literal> method.
In linear algebra, matrices don&#8217;t have the same multiplication operation as real numbers.
Two matrices can be multiplied if the first matrix has a number of columns equal to the number of rows of the second matrix.
The result of that operation is a new matrix where each element is a dot product of the corresponding row of the first matrix
and the corresponding column of the second matrix.
Here we&#8217;ve built our own implementation of the matrix to present the idea of operators overloading.
Although Python lacks a built-in type for matrices, you don&#8217;t need to build them from scratch.
The NumPy package is one of the best Python mathematical packages and among others provides native support for matrix algebra.
You can easily obtain the NumPy package from PyPI</simpara>
<simpara>En fait, l&#8217;intérêt concerne surtout la représentation de nos modèles, puisque chaque classe du modèle est représentée par
la définition d&#8217;un objet Python.
Nous pouvons donc utiliser ces mêmes <emphasis role="strong">dunder methods</emphasis> (<emphasis role="strong">double-underscores methods</emphasis>) pour étoffer les protocoles du langage.</simpara>
</section>
</section>
<section xml:id="_the_zen_of_python">
<title>The Zen of Python</title>
<programlisting language="python" linenumbering="unnumbered">&gt;&gt;&gt; import this</programlisting>
<programlisting language="text" linenumbering="unnumbered">The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!</programlisting>
</section>
<section xml:id="_pep8_style_guide_for_python_code">
<title>PEP8 - Style Guide for Python Code</title>
<simpara>La première PEP qui va nous intéresser est la <link xl:href="https://www.python.org/dev/peps/pep-0008/">PEP 8&#8201;&#8212;&#8201;Style Guide for Python Code</link>. 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, &#8230;&#8203;
En bref, elle décrit comment écrire du code proprement, afin 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é.</simpara>
<simpara>Dans cet objectif, un outil existe et listera l&#8217;ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8.
Pour l&#8217;installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (<literal>.</literal>, le nom d&#8217;un répertoire, le nom d&#8217;un fichier <literal>.py</literal>, &#8230;&#8203;).
Si vous souhaitez uniquement avoir le nombre d&#8217;erreur de chaque type, saisissez les options <literal>--statistics -qq</literal>.</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ 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 ':'</programlisting>
<simpara>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 <literal>pyflakes</literal>: cette librairie analysera vos sources à la recherche de sources d&#8217;erreurs possibles (imports inutilisés, méthodes inconnues, etc.).</simpara>
</section>
<section xml:id="_pep257_docstring_conventions">
<title>PEP257 - Docstring Conventions</title>
<simpara>Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires que nous verrons plus bas, de documenter son code.
Cela impose une certaine rigueur, mais améliore énormément la qualité, la compréhension et la reprise du code par une tierce personne.
Cela implique aussi de <emphasis role="strong">tout</emphasis> documenter: les modules, les paquets, les classes, les fonctions, méthodes, &#8230;&#8203;
Ce qui peut également aller à contrecourant d&#8217;autres pratiques cite:[clean_code(53-74)]; il y a une juste mesure à prendre entre "tout documenter" et "tout bien documenter":</simpara>
<itemizedlist>
<listitem>
<simpara>Inutile d&#8217;ajouter des watermarks, auteurs, &#8230;&#8203; Git ou tout VCS s&#8217;en sortira très bien et sera beaucoup plus efficace que n&#8217;importe quelle chaîne de caractères que vous pourriez indiquer et qui sera fausse dans six mois,</simpara>
</listitem>
<listitem>
<simpara>Inutile de décrire quelque chose qui est évident; documenter la méthode <literal>get_age()</literal> d&#8217;une personne n&#8217;aura pas beaucoup d&#8217;intérêt</simpara>
</listitem>
<listitem>
<simpara>S&#8217;il est nécessaire de décrire un comportement au sein-même d&#8217;une fonction, c&#8217;est que ce comportement pourrait être extrait dans une nouvelle fonction (qui, elle, pourra être documentée)</simpara>
</listitem>
</itemizedlist>
<warning>
<simpara>Documentation: be obsessed! Mais <emphasis role="strong">le code reste la référence</emphasis></simpara>
</warning>
<simpara>Il existe plusieurs types de conventions de documentation:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>PEP 257</simpara>
</listitem>
<listitem>
<simpara>Numpy</simpara>
</listitem>
<listitem>
<simpara>Google Style (parfois connue sous l&#8217;intitulé <literal>Napoleon</literal>)</simpara>
</listitem>
<listitem>
<simpara>&#8230;&#8203;</simpara>
</listitem>
</orderedlist>
<simpara>Les <link xl:href="https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings">conventions proposées par Google</link> nous semblent plus faciles à lire que du RestructuredText, mais sont parfois moins bien intégrées que les docstrings officiellement supportées (par exemple, <link xl:href="https://clize.readthedocs.io/en/stable/">clize</link> ne reconnait que du RestructuredText; <link xl:href="https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/">l&#8217;auto-documentation</link> de Django également).
L&#8217;exemple donné dans les guides de style de Google est celui-ci:</simpara>
<programlisting language="python" linenumbering="unnumbered">def fetch_smalltable_rows(table_handle: smalltable.Table,
keys: Sequence[Union[bytes, str]],
require_all_keys: bool = False,
) -&gt; Mapping[bytes, Tuple[str]]:
"""Fetches rows from a Smalltable.
Retrieves rows pertaining to the given keys from the Table instance
represented by table_handle. String keys will be UTF-8 encoded.
Args:
table_handle: An open smalltable.Table instance.
keys: A sequence of strings representing the key of each table
row to fetch. String keys will be UTF-8 encoded.
require_all_keys: Optional; If require_all_keys is True only
rows with values set for all keys will be returned.
Returns:
A dict mapping keys to the corresponding table row data
fetched. Each row is represented as a tuple of strings. For
example:
{b'Serak': ('Rigel VII', 'Preparer'),
b'Zim': ('Irk', 'Invader'),
b'Lrrr': ('Omicron Persei 8', 'Emperor')}
Returned keys are always bytes. If a key from the keys argument is
missing from the dictionary, then that row was not found in the
table (and require_all_keys must have been False).
Raises:
IOError: An error occurred accessing the smalltable.
"""</programlisting>
<simpara>C&#8217;est-à-dire:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Une courte ligne d&#8217;introduction, descriptive, indiquant ce que la fonction ou la méthode réalise. Attention, la documentation ne doit pas indiquer <emphasis>comment</emphasis> la fonction/méthode est implémentée, mais ce qu&#8217;elle fait concrètement (et succintement).</simpara>
</listitem>
<listitem>
<simpara>Une ligne vide</simpara>
</listitem>
<listitem>
<simpara>Une description plus complète et plus verbeuse, si vous le jugez nécessaire</simpara>
</listitem>
<listitem>
<simpara>Une ligne vide</simpara>
</listitem>
<listitem>
<simpara>La description des arguments et paramètres, des valeurs de retour, des exemples et les exceptions qui peuvent être levées.</simpara>
</listitem>
</orderedlist>
<simpara>Un exemple (encore) plus complet peut être trouvé <link xl:href="https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html#example-google">dans le dépôt sphinxcontrib-napoleon</link>.
Et ici, nous tombons peut-être dans l&#8217;excès de zèle:</simpara>
<programlisting language="python" linenumbering="unnumbered">def module_level_function(param1, param2=None, *args, **kwargs):
"""This is an example of a module level function.
Function parameters should be documented in the ``Args`` section. The name
of each parameter is required. The type and description of each parameter
is optional, but should be included if not obvious.
If \*args or \*\*kwargs are accepted,
they should be listed as ``*args`` and ``**kwargs``.
The format for a parameter is::
name (type): description
The description may span multiple lines. Following
lines should be indented. The "(type)" is optional.
Multiple paragraphs are supported in parameter
descriptions.
Args:
param1 (int): The first parameter.
param2 (:obj:`str`, optional): The second parameter. Defaults to None.
Second line of description should be indented.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
bool: True if successful, False otherwise.
The return type is optional and may be specified at the beginning of
the ``Returns`` section followed by a colon.
The ``Returns`` section may span multiple lines and paragraphs.
Following lines should be indented to match the first line.
The ``Returns`` section supports any reStructuredText formatting,
including literal blocks::
{
'param1': param1,
'param2': param2
}
Raises:
AttributeError: The ``Raises`` section is a list of all exceptions
that are relevant to the interface.
ValueError: If `param2` is equal to `param1`.
"""
if param1 == param2:
raise ValueError('param1 may not be equal to param2')
return True</programlisting>
<simpara>Pour ceux que cela pourrait intéresser, il existe <link xl:href="https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring">une extension pour Codium</link>, comme nous le verrons juste après, qui permet de générer automatiquement le squelette de documentation d&#8217;un bloc de code:</simpara>
<figure>
<title>autodocstring</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/environment/python-docstring-vscode.png"/>
</imageobject>
<textobject><phrase>python docstring vscode</phrase></textobject>
</mediaobject>
</figure>
<note>
<simpara>Nous le verrons plus loin, Django permet de rendre la documentation immédiatement accessible depuis son interface d&#8217;administration. Toute information pertinente peut donc lier le code à un cas d&#8217;utilisation concret.</simpara>
</note>
</section>
<section xml:id="_linters">
<title>Linters</title>
<simpara>Il existe plusieurs niveaux de <emphasis>linters</emphasis>:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Le premier niveau concerne <link xl:href="https://pypi.org/project/pycodestyle/">pycodestyle</link> (anciennement, <literal>pep8</literal> justement&#8230;&#8203;), qui analyse votre code à la recherche d&#8217;erreurs de convention.</simpara>
</listitem>
<listitem>
<simpara>Le deuxième niveau concerne <link xl:href="https://pypi.org/project/pyflakes/">pyflakes</link>. Pyflakes est un <emphasis>simple</emphasis> <footnote><simpara>Ce n&#8217;est pas moi qui le dit, c&#8217;est la doc du projet</simpara></footnote> programme qui recherchera des erreurs parmi vos fichiers Python.</simpara>
</listitem>
<listitem>
<simpara>Le troisième niveau est <link xl:href="https://pypi.org/project/flake8/">Flake8</link>, qui regroupe les deux premiers niveaux, en plus d&#8217;y ajouter flexibilité, extensions et une analyse de complexité de McCabe.</simpara>
</listitem>
<listitem>
<simpara>Le quatrième niveau <footnote><simpara>Oui, en Python, il n&#8217;y a que quatre cercles à l&#8217;Enfer</simpara></footnote> est <link xl:href="https://pylint.org/">PyLint</link>.</simpara>
</listitem>
</orderedlist>
<simpara>PyLint est le meilleur ami de votre <emphasis>moi</emphasis> futur, un peu comme quand vous prenez le temps de faire la vaisselle pour ne pas avoir à la faire le lendemain: il rendra votre code soyeux et brillant, en posant des affirmations spécifiques.
A vous de les traiter en corrigeant le code ou en apposant un <emphasis>tag</emphasis> indiquant que vous avez pris connaissance de la remarque, que vous en avez tenu compte, et que vous choisissez malgré tout de faire autrement.</simpara>
<simpara>Pour vous donner une idée, voici ce que cela pourrait donner avec un code pas très propre et qui ne sert à rien:</simpara>
<programlisting language="python" linenumbering="unnumbered">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)</programlisting>
<simpara>Avec Flake8, nous obtiendrons ceci:</simpara>
<programlisting language="bash" linenumbering="unnumbered">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</programlisting>
<simpara>Nous trouvons des erreurs:</simpara>
<itemizedlist>
<listitem>
<simpara>de <emphasis role="strong">conventions</emphasis>: le nombre de lignes qui séparent deux fonctions, le nombre d&#8217;espace après un opérateur, une ligne vide à la fin du fichier, &#8230;&#8203; Ces <emphasis>erreurs</emphasis> n&#8217;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.</simpara>
</listitem>
<listitem>
<simpara>de <emphasis role="strong">définition</emphasis>: une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug potentiel. Ne pas en tenir compte nuira sans doute à la santé de votre code (et risque de vous réveiller à cinq heures du mat', quand votre application se prendra méchamment les pieds dans le tapis).</simpara>
</listitem>
</itemizedlist>
<simpara>L&#8217;étape d&#8217;après consiste à invoquer pylint.
Lui, il est directement moins conciliant:</simpara>
<programlisting language="text" linenumbering="unnumbered">$ 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</programlisting>
<simpara>En gros, j&#8217;ai programmé comme une grosse bouse anémique (et oui: le score d&#8217;évaluation du code permet bien d&#8217;aller en négatif).
En vrac, nous trouvons des problèmes liés:</simpara>
<itemizedlist>
<listitem>
<simpara>au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)</simpara>
</listitem>
<listitem>
<simpara>à des variables non définies (E0602)</simpara>
</listitem>
<listitem>
<simpara>de la documentation manquante (C0114, C0116)</simpara>
</listitem>
<listitem>
<simpara>de la redéfinition de variables (W0621).</simpara>
</listitem>
</itemizedlist>
<simpara>Pour reprendre la <link xl:href="http://pylint.pycqa.org/en/latest/user_guide/message-control.html">documentation</link>, chaque code possède sa signification (ouf!):</simpara>
<itemizedlist>
<listitem>
<simpara>C convention related checks</simpara>
</listitem>
<listitem>
<simpara>R refactoring related checks</simpara>
</listitem>
<listitem>
<simpara>W various warnings</simpara>
</listitem>
<listitem>
<simpara>E errors, for probable bugs in the code</simpara>
</listitem>
<listitem>
<simpara>F fatal, if an error occurred which prevented pylint from doing further* processing.</simpara>
</listitem>
</itemizedlist>
<simpara>TODO: Expliquer comment faire pour tagger une explication.</simpara>
<simpara>TODO: Voir si la sortie de pylint est obligatoirement 0 s&#8217;il y a un warning</simpara>
<simpara>TODO: parler de <literal>pylint --errors-only</literal></simpara>
</section>
<section xml:id="_formatage_de_code">
<title>Formatage de code</title>
<simpara>Nous avons parlé ci-dessous de style de codage pour Python (PEP8), de style de rédaction pour la documentation (PEP257), d&#8217;un <emphasis>linter</emphasis> pour nous indiquer quels morceaux de code doivent absolument être revus, &#8230;&#8203;
Reste que ces tâches sont <phrase role="line-through">parfois</phrase> (très) souvent fastidieuses: écrire un code propre et systématiquement cohérent est une tâche ardue.
Heureusement, il existe des outils pour nous aider (un peu).</simpara>
<simpara>A nouveau, il existe plusieurs possibilités de formatage automatique du code.
Même si elle n&#8217;est pas parfaite, <link xl:href="https://black.readthedocs.io/en/stable/">Black</link> arrive à un compromis entre clarté du code, facilité d&#8217;installation et d&#8217;intégration et résultat.</simpara>
<simpara>Est-ce que ce formatage est idéal et accepté par tout le monde ?
<emphasis role="strong">Non</emphasis>. Même Pylint arrivera parfois à râler.
Mais ce formatage conviendra dans 97,83% des cas (au moins).</simpara>
<blockquote>
<simpara>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.</simpara>
<simpara>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.</simpara>
</blockquote>
<simpara>Traduit rapidement à partir de la langue de Batman: "<emphasis>En utilisant Black, vous cédez le contrôle sur le formatage de votre code. En retour, Black vous fera gagner un max de temps, diminuera votre charge mentale et fera revenir l&#8217;être aimé</emphasis>".
Mais la partie réellement intéressante concerne le fait que "<emphasis>Tout code qui sera passé par Black aura la même forme, indépendamment du project sur lequel vous serez en train de travailler. L&#8217;étape de formatage deviendra transparente, et vous pourrez vous concentrer sur le contenu</emphasis>".</simpara>
</section>
<section xml:id="_complexité_cyclomatique">
<title>Complexité cyclomatique</title>
<simpara>A nouveau, un greffon pour <literal>flake8</literal> existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec <literal>pip install mccabe</literal>, et activez-le avec le paramètre <literal>--max-complexity</literal>. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.</simpara>
</section>
<section xml:id="_typage_statique_pep585">
<title>Typage statique - <link xl:href="https://www.python.org/dev/peps/pep-0585/">PEP585</link></title>
<simpara>Nous vous disions ci-dessus que Python était un langage dynamique interprété.
Concrètement, cela signifie que des erreurs pouvant être détectées à la compilation avec d&#8217;autres langages, ne le sont pas avec Python.</simpara>
<simpara>Il existe cependant une solution à ce problème, sous la forme de <link xl:href="http://mypy-lang.org/">Mypy</link>, qui peut (sous vous le souhaitez ;-)) vérifier une forme de typage statique de votre code source, grâce à une expressivité du code, basée sur des annotations (facultatives, elles aussi).</simpara>
<simpara>Ces vérifications se présentent de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered">from typing import List
def first_int_elem(l: List[int]) -&gt; int:
return l[0] if l else None
if __name__ == "__main__":
print(first_int_elem([1, 2, 3]))
print(first_int_elem(['a', 'b', 'c']))</programlisting>
<simpara>Est-ce que le code ci-dessous fonctionne correctement ?
<emphasis role="strong">Oui</emphasis>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">λ python mypy-test.py
1
a</programlisting>
<simpara>Malgré que nos annotations déclarent une liste d&#8217;entiers, rien ne nous empêche de lui envoyer une liste de caractères, sans que cela ne lui pose de problèmes.</simpara>
<simpara>Est-ce que Mypy va râler ? <emphasis role="strong">Oui, aussi</emphasis>.
Non seulement nous retournons la valeur <literal>None</literal> si la liste est vide alors que nous lui annoncions un entier en sortie, mais en plus, nous l&#8217;appelons avec une liste de caractères, alors que nous nous attendions à une liste d&#8217;entiers:</simpara>
<programlisting language="bash" linenumbering="unnumbered">λ mypy mypy-test.py
mypy-test.py:7: error: Incompatible return value type (got "Optional[int]", expected "int")
mypy-test.py:12: error: List item 0 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 1 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 2 has incompatible type "str"; expected "int"
Found 4 errors in 1 file (checked 1 source file)</programlisting>
<simpara>Pour corriger ceci, nous devons:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Importer le type <literal>Optional</literal> et l&#8217;utiliser en sortie de notre fonction <literal>first_int_elem</literal></simpara>
</listitem>
<listitem>
<simpara>Eviter de lui donner de
mauvais paramètres ;-)</simpara>
</listitem>
</orderedlist>
<programlisting language="python" linenumbering="unnumbered">from typing import List, Optional
def first_int_elem(l: List[int]) -&gt; Optional[int]:
return l[0] if l else None
if __name__ == "__main__":
print(first_int_elem([1, 2, 3]))</programlisting>
<programlisting language="bash" linenumbering="unnumbered">λ mypy mypy-test.py
Success: no issues found in 1 source file</programlisting>
<section xml:id="_tests_unitaires">
<title>Tests unitaires</title>
<simpara><emphasis role="strong">&#8594; PyTest</emphasis></simpara>
<simpara>Comme tout bon <emphasis role="strong">framework</emphasis> qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier <emphasis role="strong">tests.py</emphasis>, qui inclut la classe <literal>TestCase</literal> depuis le package <literal>django.test</literal>:</simpara>
<programlisting language="python" linenumbering="unnumbered">from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')</programlisting>
<simpara>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&#8217;isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l&#8217;ajout d&#8217;une nouvelle fonctionnalité ou de la modification d&#8217;une existante.
Il existe plusieurs types de tests (intégration, comportement, &#8230;&#8203;); on ne parlera ici que des tests unitaires.</simpara>
<simpara>Avoir des tests, c&#8217;est bien.
S&#8217;assurer que tout est testé, c&#8217;est mieux.
C&#8217;est là qu&#8217;il est utile d&#8217;avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.</simpara>
<simpara>Comme indiqué ci-dessus, Django propose son propre cadre de tests, au travers du package <literal>django.tests</literal>.
Une bonne pratique (parfois discutée) consiste cependant à switcher vers <literal>pytest</literal>, qui présente quelques avantages:</simpara>
<itemizedlist>
<listitem>
<simpara>Une syntaxe plus concise (au prix de <link xl:href="https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html">quelques conventions</link>, même si elles restent configurables): un test est une fonction, et ne doit pas obligatoirement faire partie d&#8217;une classe héritant de <literal>TestCase</literal> - la seule nécessité étant que cette fonction fasse partie d&#8217;un module commençant ou finissant par "test" (<literal>test_example.py</literal> ou <literal>example_test.py</literal>).</simpara>
</listitem>
<listitem>
<simpara>Une compatibilité avec du code Python "classique" - vous ne devrez donc retenir qu&#8217;un seul ensemble de commandes ;-)</simpara>
</listitem>
<listitem>
<simpara>Des <emphasis>fixtures</emphasis> faciles à réutiliser entre vos différents composants</simpara>
</listitem>
<listitem>
<simpara>Une compatibilité avec le reste de l&#8217;écosystème, dont la couverture de code présentée ci-dessous.</simpara>
</listitem>
</itemizedlist>
<simpara>Ainsi, après installation, il nous suffit de créer notre module <literal>test_models.py</literal>, dans lequel nous allons simplement tester l&#8217;addition d&#8217;un nombre et d&#8217;une chaîne de caractères (oui, c&#8217;est complètement biesse; on est sur la partie théorique ici):</simpara>
<programlisting language="python" linenumbering="unnumbered">def test_add():
assert 1 + 1 == "argh"</programlisting>
<simpara>Forcément, cela va planter.
Pour nous en assurer (dès fois que quelqu&#8217;un en doute), il nous suffit de démarrer la commande <literal>pytest</literal>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">λ pytest
============================= test session starts ====================================
platform ...
rootdir: ...
plugins: django-4.1.0
collected 1 item
gwift\test_models.py F [100%]
================================== FAILURES ==========================================
_______________________________ test_basic_add _______________________________________
def test_basic_add():
&gt; assert 1 + 1 == "argh"
E AssertionError: assert (1 + 1) == 'argh'
gwift\test_models.py:2: AssertionError
=========================== short test summary info ==================================
FAILED gwift/test_models.py::test_basic_add - AssertionError: assert (1 + 1) == 'argh'
============================== 1 failed in 0.10s =====================================</programlisting>
</section>
<section xml:id="_couverture_de_code">
<title>Couverture de code</title>
<simpara>La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests.
Attention qu&#8217;il ne s&#8217;agit pas de vérifier que le code est <emphasis role="strong">bien</emphasis> testé, mais juste de vérifier <emphasis role="strong">quelle partie</emphasis> du code est testée.
Le paquet <literal>coverage</literal> se charge d&#8217;évaluer le pourcentage de code couvert par les tests.</simpara>
<simpara>Avec <literal>pytest</literal>, il convient d&#8217;utiliser le paquet <link xl:href="https://pypi.org/project/pytest-cov/"><literal>pytest-cov</literal></link>, suivi de la commande <literal>pytest --cov=gwift tests/</literal>.</simpara>
<simpara>Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer par le paquet <link xl:href="https://pypi.org/project/django-coverage-plugin/">django-coverage-plugin</link> Ajoutez-le dans le fichier <literal>requirements/base.txt</literal>, et lancez une couverture de code grâce à la commande <literal>coverage</literal>.
La configuration peut se faire dans un fichier <literal>.coveragerc</literal> que vous placerez à la racine de votre projet, et qui sera lu lors de l&#8217;exécution.</simpara>
<programlisting language="bash" linenumbering="unnumbered"># requirements/base.text
[...]
django_coverage_plugin</programlisting>
<programlisting language="bash" linenumbering="unnumbered"># .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[report]
ignore_errors = True
[html]
directory = coverage_html_report</programlisting>
<programlisting language="bash" linenumbering="unnumbered">$ 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</programlisting>
<simpara>&#8592;-- / partie obsolète --&#8594;</simpara>
<simpara>Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes.</simpara>
</section>
<section xml:id="_matrice_de_compatibilité">
<title>Matrice de compatibilité</title>
<simpara>L&#8217;intérêt de la matrice de compatibilité consiste à spécifier un ensemble de plusieurs versions d&#8217;un même interpréteur (ici, Python), afin de s&#8217;assurer que votre application continue à fonctionner. Nous sommes donc un cran plus haut que la spécification des versions des librairies, puisque nous nous situons directement au niveau de l&#8217;interpréteur.</simpara>
<simpara>L&#8217;outil le plus connu est <link xl:href="https://tox.readthedocs.io/en/latest/">Tox</link>, qui consiste en un outil basé sur virtualenv et qui permet:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>de vérifier que votre application s&#8217;installe correctement avec différentes versions de Python et d&#8217;interpréteurs</simpara>
</listitem>
<listitem>
<simpara>de démarrer des tests parmi ces différents environnements</simpara>
</listitem>
</orderedlist>
<programlisting language="ini" linenumbering="unnumbered"># content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py36,py37,py38,py39
skipsdist = true
[testenv]
deps =
-r requirements/dev.txt
commands =
pytest</programlisting>
<simpara>Démarrez ensuite la commande <literal>tox</literal>, pour démarrer la commande <literal>pytest</literal> sur les environnements Python 3.6, 3.7, 3.8 et 3.9, après avoir installé nos dépendances présentes dans le fichier <literal>requirements/dev.txt</literal>.</simpara>
<warning>
<simpara>pour que la commande ci-dessus fonctionne correctement, il sera nécessaire que vous ayez les différentes versions d&#8217;interpréteurs installées.
Ci-dessus, la commande retournera une erreur pour chaque version non trouvée, avec une erreur type <literal>ERROR: pyXX: InterpreterNotFound: pythonX.X</literal>.</simpara>
</warning>
</section>
<section xml:id="_configuration_globale">
<title>Configuration globale</title>
<simpara>Décrire le fichier setup.cfg</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ touch setup.cfg</programlisting>
</section>
<section xml:id="_dockerfile">
<title>Dockerfile</title>
<programlisting language="docker" linenumbering="unnumbered"># Dockerfile
# Pull base image
#FROM python:3.8
FROM python:3.8-slim-buster
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV DEBIAN_FRONTEND noninteractive
ENV ACCEPT_EULA=Y
# install Microsoft SQL Server requirements.
ENV ACCEPT_EULA=Y
RUN apt-get update -y &amp;&amp; apt-get update \
&amp;&amp; apt-get install -y --no-install-recommends curl gcc g++ gnupg
# Add SQL Server ODBC Driver 17
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/debian/10/prod.list &gt; /etc/apt/sources.list.d/mssql-release.list
RUN apt-get update \
&amp;&amp; apt-get install -y msodbcsql17 unixodbc-dev
# clean the install.
RUN apt-get -y clean
# Set work directory
WORKDIR /code
# Install dependencies
COPY ./requirements/base.txt /code/requirements/
RUN pip install --upgrade pip
RUN pip install -r ./requirements/base.txt
# Copy project
COPY . /code/</programlisting>
</section>
<section xml:id="_makefile">
<title>Makefile</title>
<simpara>Pour gagner un peu de temps, n&#8217;hésitez pas à créer un fichier <literal>Makefile</literal> que vous placerez à la racine du projet.
L&#8217;exemple ci-dessous permettra, grâce à la commande <literal>make coverage</literal>, d&#8217;arriver au même résultat que ci-dessus:</simpara>
<programlisting language="text" linenumbering="unnumbered"># Makefile for gwift
#
# User-friendly check for coverage
ifeq ($(shell which coverage &gt;/dev/null 2&gt;&amp;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."</programlisting>
<simpara>Pour la petite histoire, <literal>make</literal> peu sembler un peu désuet, mais reste extrêmement efficace.</simpara>
</section>
</section>
<section xml:id="_environnement_de_développement">
<title>Environnement de développement</title>
<simpara>Concrètement, nous pourrions tout à fait nous limiter à Notepad ou Notepad++.
Mais à moins d&#8217;aimer se fouetter avec un câble USB, nous apprécions la complétion du code, la coloration syntaxique, l&#8217;intégration des tests unitaires et d&#8217;un debugger, ainsi que deux-trois sucreries qui feront plaisir à n&#8217;importe quel développeur.</simpara>
<simpara>Si vous manquez d&#8217;idées ou si vous ne savez pas par où commencer:</simpara>
<itemizedlist>
<listitem>
<simpara><link xl:href="https://vscodium.com/">VSCodium</link>, avec les plugins <link xl:href="https://marketplace.visualstudio.com/items?itemName=ms-python.python">Python</link>et <link xl:href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">GitLens</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://www.jetbrains.com/pycharm/">PyCharm</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://www.vim.org/">Vim</link> avec les plugins <link xl:href="https://github.com/davidhalter/jedi-vim">Jedi-Vim</link> et <link xl:href="https://github.com/preservim/nerdtree">nerdtree</link></simpara>
</listitem>
</itemizedlist>
<simpara>Si vous hésitez, et même si Codium n&#8217;est pas le plus léger (la faute à <link xl:href="https://www.electronjs.org/">Electron</link>&#8230;&#8203;), il fera correctement son travail (à savoir: faciliter le vôtre), en intégrant suffisament de fonctionnalités qui gâteront les papilles émoustillées du développeur impatient.</simpara>
<figure>
<title>Codium en action</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/environment/codium.png"/>
</imageobject>
<textobject><phrase>codium</phrase></textobject>
</mediaobject>
</figure>
</section>
<section xml:id="_un_terminal">
<title>Un terminal</title>
<simpara><emphasis>A priori</emphasis>, les IDE <footnote><simpara>Integrated Development Environment</simpara></footnote> proposés ci-dessus fournissent par défaut ou <emphasis>via</emphasis> des greffons un terminal intégré.
Ceci dit, disposer d&#8217;un terminal séparé facilite parfois certaines tâches.</simpara>
<simpara>A nouveau, si vous manquez d&#8217;idées:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Si vous êtes sous Windows, téléchargez une copie de <link xl:href="https://cmder.net/">Cmder</link>. Il n&#8217;est pas le plus rapide, mais propose une intégration des outils Unix communs (<literal>ls</literal>, <literal>pwd</literal>, <literal>grep</literal>, <literal>ssh</literal>, <literal>git</literal>, &#8230;&#8203;) sans trop se fouler.</simpara>
</listitem>
<listitem>
<simpara>Pour tout autre système, vous devriez disposer en natif de ce qu&#8217;il faut.</simpara>
</listitem>
</orderedlist>
<figure>
<title>Mise en abîme</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/environment/terminal.png"/>
</imageobject>
<textobject><phrase>terminal</phrase></textobject>
</mediaobject>
</figure>
</section>
<section xml:id="_un_gestionnaire_de_base_de_données">
<title>Un gestionnaire de base de données</title>
<simpara>Django gère plusieurs moteurs de base de données.
Certains sont gérés nativement par Django (PostgreSQL, MariaDB, SQLite); <emphasis>a priori</emphasis>, ces trois-là sont disponibles pour tous les systèmes d&#8217;exploitation. D&#8217;autres moteurs nécessitent des librairies tierces (Oracle, Microsoft SQL Server).</simpara>
<simpara>Il n&#8217;est pas obligatoire de disposer d&#8217;une application de gestion pour ces moteurs: pour les cas d&#8217;utilisation simples, le shell Django pourra largement suffire (nous y reviendrons).
Mais pour faciliter la gestion des bases de données elles-même, et si vous n&#8217;êtes pas à l&#8217;aise avec la ligne de commande, choisissez l&#8217;une des applications d&#8217;administration ci-dessous en fonction du moteur de base de données que vous souhaitez utiliser.</simpara>
<itemizedlist>
<listitem>
<simpara>Pour <emphasis role="strong">PostgreSQL</emphasis>, il existe <link xl:href="https://www.pgadmin.org/">pgAdmin</link></simpara>
</listitem>
<listitem>
<simpara>Pour <emphasis role="strong">MariaDB</emphasis> ou <emphasis role="strong">MySQL</emphasis>, partez sur <link xl:href="https://www.phpmyadmin.net/">PHPMyAdmin</link></simpara>
</listitem>
<listitem>
<simpara>Pour <emphasis role="strong">SQLite</emphasis>, il existe <link xl:href="https://sqlitebrowser.org/">SQLiteBrowser</link>
PHPMyAdmin ou PgAdmin.</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_un_gestionnaire_de_mots_de_passe">
<title>Un gestionnaire de mots de passe</title>
<simpara>Nous en auront besoin pour gé(né)rer des phrases secrètes pour nos applications.
Si vous n&#8217;en utilisez pas déjà un, partez sur <link xl:href="https://keepassxc.org/">KeepassXC</link>: il est multi-plateformes, suivi et s&#8217;intègre correctement aux différents environnements, tout en restant accessible.</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/environment/keepass.png"/>
</imageobject>
<textobject><phrase>keepass</phrase></textobject>
</mediaobject>
</informalfigure>
</section>
<section xml:id="_un_système_de_gestion_de_versions">
<title>Un système de gestion de versions</title>
<simpara>Il existe plusieurs systèmes de gestion de versions.
Le plus connu à l&#8217;heure actuelle est <link xl:href="https://git-scm.com/">Git</link>, notamment pour sa (très) grande flexibilité et sa rapidité d&#8217;exécution.
Il est une aide précieuse pour développer rapidement des preuves de concept, switcher vers une nouvelle fonctionnalité, un bogue à réparer ou une nouvelle release à proposer au téléchargement.
Ses deux plus gros défauts concerneraient peut-être sa courbe d&#8217;apprentissage pour les nouveaux venus et la complexité des actions qu&#8217;il permet de réaliser.</simpara>
<figure>
<title><link xl:href="https://xkcd.com/1597/">https://xkcd.com/1597/</link></title>
<mediaobject>
<imageobject>
<imagedata fileref="images/xkcd-1597-git.png"/>
</imageobject>
<textobject><phrase>xkcd 1597 git</phrase></textobject>
</mediaobject>
</figure>
<simpara>Même pour un développeur solitaire, un système de gestion de versions (quel qu&#8217;il soit) reste indispensable.</simpara>
<simpara>Chaque "<emphasis role="strong">branche</emphasis>" correspond à une tâche à réaliser: un bogue à corriger (<emphasis>Hotfix A</emphasis>), une nouvelle fonctionnalité à ajouter ou un "<emphasis>truc à essayer</emphasis>" <footnote><simpara>Oui, comme dans "Attends, j&#8217;essaie vite un truc, si ça marche, c&#8217;est beau."</simpara></footnote> (<emphasis>Feature A</emphasis> et <emphasis>Feature B</emphasis>).</simpara>
<simpara>Chaque "<emphasis role="strong">commit</emphasis>" correspond à une sauvegarde atomique d&#8217;un état ou d&#8217;un ensemble de modifications cohérentes entre elles.<footnote><simpara>Il convient donc de s&#8217;abstenir de modifier le CSS d&#8217;une application et la couche d&#8217;accès à la base de données, sous peine de se faire huer par ses relecteurs au prochain stand-up.</simpara></footnote>
De cette manière, il est beaucoup plus facile pour le développeur de se concenter sur un sujet en particulier, dans la mesure où celui-ci ne doit pas obligatoirement être clôturé pour appliquer un changement de contexte.</simpara>
<figure>
<title>Git en action</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/diagrams/git-workflow.png"/>
</imageobject>
<textobject><phrase>git workflow</phrase></textobject>
</mediaobject>
</figure>
<simpara>Cas pratique: vous développez cette nouvelle fonctionnalité qui va révolutionner le monde de demain et d&#8217;après-demain, quand, tout à coup (!), vous vous rendez compte que vous avez perdu votre conformité aux normes PCI parce les données des titulaires de cartes ne sont pas isolées correctement.
Il suffit alors de:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>sauver le travail en cours (<literal>git add . &amp;&amp; git commit -m [WIP]</literal>)</simpara>
</listitem>
<listitem>
<simpara>revenir sur la branche principale (<literal>git checkout main</literal>)</simpara>
</listitem>
<listitem>
<simpara>créer un "hotfix" (<literal>git checkout -b hotfix/pci-compliance</literal>)</simpara>
</listitem>
<listitem>
<simpara>solutionner le problème (sans doute un <literal>;</literal> en trop ?)</simpara>
</listitem>
<listitem>
<simpara>sauver le correctif sur cette branche (<literal>git add . &amp;&amp; git commit -m "Did it!"</literal>)</simpara>
</listitem>
<listitem>
<simpara>récupérer ce correctif sur la branche principal (<literal>git checkout main &amp;&amp; git merge hotfix/pci-compliance</literal>)</simpara>
</listitem>
<listitem>
<simpara>et revenir tranquillou sur votre branche de développement pour fignoler ce générateur de noms de dinosaures rigolos que l&#8217;univers vous réclame à cor et à a cri (<literal>git checkout features/dinolol</literal>)</simpara>
</listitem>
</orderedlist>
<simpara>Finalement, sachez qu&#8217;il existe plusieurs manières de gérer ces flux d&#8217;informations.
Les plus connus sont <link xl:href="https://www.gitflow.com/">Gitflow</link> et <link xl:href="https://www.reddit.com/r/programming/comments/7mfxo6/a_branching_strategy_simpler_than_gitflow/">Threeflow</link>.</simpara>
<section xml:id="_décrire_ses_changements">
<title>Décrire ses changements</title>
<simpara>La description d&#8217;un changement se fait <emphasis>via</emphasis> la commande <literal>git commit</literal>.
Il est possible de lui passer directement le message associé à ce changement grâce à l&#8217;attribut <literal>-m</literal>, mais c&#8217;est une pratique relativement déconseillée: un <emphasis>commit</emphasis> ne doit effectivement pas obligatoirement être décrit sur une seule ligne.
Une description plus complète, accompagnée des éventuels tickets ou références, sera plus complète, plus agréable à lire, et plus facile à revoir pour vos éventuels relecteurs.</simpara>
<simpara>De plus, la plupart des plateformes de dépôts présenteront ces informations de manière ergonomique. Par exemple:</simpara>
<figure>
<title>Un exemple de commit affiché dans Gitea</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/environment/gitea-commit-message.png"/>
</imageobject>
<textobject><phrase>gitea commit message</phrase></textobject>
</mediaobject>
</figure>
<simpara>La première ligne est reprise comme titre (normalement, sur 50 caractères maximum); le reste est repris comme de la description.</simpara>
</section>
</section>
<section xml:id="_un_système_de_virtualisation">
<title>Un système de virtualisation</title>
<simpara>Par "<emphasis>système de virtualisation</emphasis>", nous entendons n&#8217;importe quel application, système d&#8217;exploitation, système de containeurisation, &#8230;&#8203; qui permette de créer ou recréer un environnement de développement aussi proche que celui en production.
Les solutions sont nombreuses:</simpara>
<itemizedlist>
<listitem>
<simpara><link xl:href="https://www.virtualbox.org/">VirtualBox</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://www.vagrantup.com/">Vagrant</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://www.docker.com/">Docker</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://linuxcontainers.org/lxc/">Linux Containers (LXC)</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v">Hyper-V</link></simpara>
</listitem>
</itemizedlist>
<simpara>Ces quelques propositions se situent un cran plus loin que la "simple" isolation d&#8217;un environnement, puisqu&#8217;elles vous permettront de construire un environnement complet.
Elles constituent donc une étape supplémentaires dans la configuration de votre espace de travail, mais en amélioreront la qualité.</simpara>
<simpara>Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux solutions automatisables et multiplateformes, dont la configuration peut faire partie intégrante de vos sources.</simpara>
<section xml:id="_vagrant">
<title>Vagrant</title>
<simpara>Vagrant consiste en un outil de création et de gestion d&#8217;environnements virtualisés, en respectant toujours une même manière de travailler, indépendamment des choix techniques et de l&#8217;infrastructure que vous pourriez sélectionner.</simpara>
<blockquote>
<simpara>Vagrant is a tool for building and managing virtual machine environments in a single workflow. With an easy-to-use workflow and focus on automation, Vagrant lowers development environment setup time, increases production parity, and makes the "works on my machine" excuse a relic of the past. <footnote><simpara><link xl:href="https://www.vagrantup.com/intro">https://www.vagrantup.com/intro</link></simpara></footnote></simpara>
</blockquote>
<simpara>La partie la plus importante de la configuration de Vagrant pour votre projet consiste à placer un fichier <literal>Vagrantfile</literal> - <emphasis>a priori</emphasis> à la racine de votre projet - et qui contiendra les information suivantes:</simpara>
<itemizedlist>
<listitem>
<simpara>Le choix du <emphasis>fournisseur</emphasis> (<emphasis role="strong">provider</emphasis>) de virtualisation (Virtualbox, Hyper-V et Docker sont natifs; il est également possible de passer par VMWare, AWS, etc.)</simpara>
</listitem>
<listitem>
<simpara>Une <emphasis>box</emphasis>, qui consiste à lui indiquer le type et la version attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et <link xl:href="https://app.vagrantup.com/boxes/search">il y a du choix</link>).</simpara>
</listitem>
<listitem>
<simpara>La manière dont la fourniture (<emphasis role="strong">provisioning</emphasis>) de l&#8217;environnement doit être réalisée: scripts Shell, fichiers, Ansible, Puppet, Chef, &#8230;&#8203; Choisissez votre favori :-) même s&#8217;il est toujours possible de passer par une installation et une maintenance manuelle, après s&#8217;être connecté sur la machine.</simpara>
</listitem>
<listitem>
<simpara>Si un espace de stockage doit être partagé entre la machine virtuelle et l&#8217;hôte</simpara>
</listitem>
<listitem>
<simpara>Les ports qui doivent être transmis de la machine virtuelle vers l&#8217;hôte.</simpara>
</listitem>
</itemizedlist>
<simpara>La syntaxe de ce fichier <literal>Vagrantfile</literal> est en <link xl:href="https://www.ruby-lang.org/en/">Ruby</link>. Vous trouverez ci-dessous un exemple, généré (et nettoyé) après avoir exécuté la commande <literal>vagrant init</literal>:</simpara>
<programlisting language="ruby" linenumbering="unnumbered"># -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
config.vm.provider "virtualbox" do |vb|
vb.gui = true
vb.memory = "1024"
end
config.vm.provision "shell", inline: &lt;&lt;-SHELL
apt-get update
apt-get install -y nginx
SHELL
end</programlisting>
<simpara>Dans le fichier ci-dessus, nous créons:</simpara>
<itemizedlist>
<listitem>
<simpara>Une nouvelle machine virtuelle (ie. <emphasis>invitée</emphasis>) sous Ubuntu Bionic Beaver, en x64</simpara>
</listitem>
<listitem>
<simpara>Avec une correspondance du port <literal>80</literal> de la machine vers le port <literal>8080</literal> de l&#8217;hôte, en limitant l&#8217;accès à celui-ci - accédez à <literal>localhost:8080</literal> et vous accéderez au port <literal>80</literal> de la machine virtuelle.</simpara>
</listitem>
<listitem>
<simpara>En utilisant Virtualbox comme backend - la mémoire vive allouée sera limitée à 1Go de RAM et nous ne voulons pas voir l&#8217;interface graphique au démarrage</simpara>
</listitem>
<listitem>
<simpara>Et pour finir, nous voulons appliquer un script de mise à jour <literal>apt-get update</literal> et installer le paquet <literal>nginx</literal></simpara>
</listitem>
</itemizedlist>
<note>
<simpara>Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichier <literal>Vagrantfile</literal> se trouve) sera synchronisé dans le répertoire <literal>/vagrant</literal> sur la machine invitée.</simpara>
</note>
</section>
<section xml:id="_docker">
<title>Docker</title>
<simpara>(copié/collé de cookie-cutter-django)</simpara>
<programlisting language="dockerfile" linenumbering="unnumbered">version: '3'
volumes:
local_postgres_data: {}
local_postgres_data_backups: {}
services:
django: &amp;django
build:
context: .
dockerfile: ./compose/local/django/Dockerfile
image: khana_local_django
container_name: django
depends_on:
- postgres
volumes:
- .:/app:z
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
ports:
- "8000:8000"
command: /start
postgres:
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
image: khana_production_postgres
container_name: postgres
volumes:
- local_postgres_data:/var/lib/postgresql/data:Z
- local_postgres_data_backups:/backups:z
env_file:
- ./.envs/.local/.postgres
docs:
image: khana_local_docs
container_name: docs
build:
context: .
dockerfile: ./compose/local/docs/Dockerfile
env_file:
- ./.envs/.local/.django
volumes:
- ./docs:/docs:z
- ./config:/app/config:z
- ./khana:/app/khana:z
ports:
- "7000:7000"
command: /start-docs
redis:
image: redis:5.0
container_name: redis
celeryworker:
&lt;&lt;: *django
image: khana_local_celeryworker
container_name: celeryworker
depends_on:
- redis
- postgres
ports: []
command: /start-celeryworker
celerybeat:
&lt;&lt;: *django
image: khana_local_celerybeat
container_name: celerybeat
depends_on:
- redis
- postgres
ports: []
command: /start-celerybeat
flower:
&lt;&lt;: *django
image: khana_local_flower
container_name: flower
ports:
- "5555:5555"
command: /start-flower</programlisting>
<programlisting language="dockerfile" linenumbering="unnumbered"># docker-compose.yml
version: '3.8'
services:
web:
build: .
command: python /code/manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- 8000:8000
depends_on:
- slqserver
slqserver:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
- "ACCEPT_EULA=Y"
- "SA_PASSWORD=sqklgjqihagrtdgqk12§!"
ports:
- 1433:1433
volumes:
- ../sqlserver/data:/var/opt/mssql/data
- ../sqlserver/log:/var/opt/mssql/log
- ../sqlserver/secrets:/var/opt/mssql/secrets</programlisting>
<programlisting language="dockerfile" linenumbering="unnumbered">FROM python:3.8-slim-buster
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
RUN apt-get update \
# dependencies for building Python packages
&amp;&amp; apt-get install -y build-essential \
# psycopg2 dependencies
&amp;&amp; apt-get install -y libpq-dev \
# Translations dependencies
&amp;&amp; apt-get install -y gettext \
# cleaning up unused files
&amp;&amp; apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&amp;&amp; rm -rf /var/lib/apt/lists/*
# Requirements are installed here to ensure they will be cached.
COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt
COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
COPY ./compose/local/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r$//g' /start-celeryworker
RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r$//g' /start-celerybeat
RUN chmod +x /start-celerybeat
COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r$//g' /start-flower
RUN chmod +x /start-flower
WORKDIR /app
ENTRYPOINT ["/entrypoint"]</programlisting>
<note>
<simpara>Voir comment nous pouvons intégrer toutes ces commandes au niveau de la CI et au niveau du déploiement (Docker-compose ?)</simpara>
</note>
</section>
<section xml:id="_base_de_données">
<title>Base de données</title>
<simpara>Parfois, SQLite peut être une bonne option:</simpara>
<blockquote>
<simpara>Write througput is the area where SQLite struggles the most, but there&#8217;s not a ton of compelling data online about how it fares, so I got some of my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly modified1 version of the SQLite kvtest2 program on it. Writing 512 byte blobs as separate transactions, in WAL mode with synchronous=normal3, temp_store=memory, and mmap enabled, I got 13.78μs per write, or ~72,568 writes per second. Going a bit larger, at 32kb writes, I got 303.74μs per write, or ~3,292 writes per second. That&#8217;s not astronomical, but it&#8217;s certainly way more than most websites being used by humans need. If you had 10 million daily active users, each one could get more than 600 writes per day with that.</simpara>
</blockquote>
<blockquote>
<simpara>Looking at read throughput, SQLite can go pretty far: with the same test above, I got a read throughput of ~496,770 reads/sec (2.013μs/read) for the 512 byte blob. Other people also report similar results — Expensify reports that you can get 4M QPS if you&#8217;re willing to make some slightly more involved changes and use a beefier server. Four million QPS is enough that every internet user in the world could make ~70 queries per day, with a little headroom left over4. Most websites don&#8217;t need that kind of throughput. cite:[consider_sqlite]</simpara>
</blockquote>
</section>
</section>
</chapter>
<chapter xml:id="_démarrer_un_nouveau_projet">
<title>Démarrer un nouveau projet</title>
<section xml:id="_travailler_en_isolation">
<title>Travailler en isolation</title>
<simpara>Nous allons aborder la gestion et l&#8217;isolation des dépendances.
Cette section est aussi utile pour une personne travaillant seule, que pour transmettre les connaissances à un nouveau membre de l&#8217;équipe ou pour déployer l&#8217;application elle-même.</simpara>
<simpara>Il en était déjà question au deuxième point des 12 facteurs: même dans le cas de petits projets, il est déconseillé de s&#8217;en passer.
Cela évite les déploiements effectués à l&#8217;arrache à grand renfort de <literal>sudo</literal> et d&#8217;installation globale de dépendances, pouvant potentiellement occasioner des conflits entre les applications déployées:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>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&#8217;une même dépendance.</simpara>
</listitem>
<listitem>
<simpara>Pour la reproductibilité d&#8217;un environnement spécifique, cela évite notamment les réponses type "Ca juste marche chez moi", puisque la construction d&#8217;un nouvel environnement fait partie intégrante du processus de construction et de la documentation du projet; grâce à elle, nous avons la possibilité de construire un environnement sain et d&#8217;appliquer des dépendances identiques, quelle que soit la machine hôte.</simpara>
</listitem>
</orderedlist>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/it-works-on-my-machine.jpg"/>
</imageobject>
<textobject><phrase>it works on my machine</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>Dans la suite de ce chapitre, nous allons considérer deux projets différents:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Gwift, une application permettant de gérer des listes de souhaits</simpara>
</listitem>
<listitem>
<simpara>Khana, une application de suivi d&#8217;apprentissage pour des élèves ou étudiants.</simpara>
</listitem>
</orderedlist>
<section xml:id="_roulements_de_versions">
<title>Roulements de versions</title>
<simpara>Django fonctionne sur un <link xl:href="https://docs.djangoproject.com/en/dev/internals/release-process/">roulement de trois versions mineures pour une version majeure</link>,
clôturé par une version LTS (<emphasis>Long Term Support</emphasis>).</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/django-support-lts.png"/>
</imageobject>
<textobject><phrase>django support lts</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>La version utilisée sera une bonne indication à prendre en considération pour nos dépendances,
puisqu&#8217;en visant une version particulière, nous ne devrons pratiquement pas nous soucier
(bon, un peu quand même, mais nous le verrons plus tard&#8230;&#8203;) des dépendances à installer,
pour peu que l&#8217;on reste sous un certain seuil.</simpara>
<simpara>Dans les étapes ci-dessous, nous épinglerons une version LTS afin de nous assurer une certaine sérénité d&#8217;esprit (= dont nous
ne occuperons pas pendant les 3 prochaines années).</simpara>
</section>
<section xml:id="_environnements_virtuels">
<title>Environnements virtuels</title>
<figure>
<title><link xl:href="https://xkcd.com/1987">https://xkcd.com/1987</link></title>
<mediaobject>
<imageobject>
<imagedata fileref="images/xkcd-1987.png"/>
</imageobject>
<textobject><phrase>xkcd 1987</phrase></textobject>
</mediaobject>
</figure>
<simpara>Un des reproches que l&#8217;on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont pas toujours simples ou directes.
Pour quelqu&#8217;un qui débarquererait, la quantité d&#8217;options différentes peut paraître rebutante.
Nous pensons notamment aux environnements virtuels: ils sont géniaux à utiliser, mais on est passé par virtualenv (l&#8217;ancêtre), virtualenvwrapper (sa version améliorée et plus ergonomique), <literal>venv</literal> (la version intégrée depuis la version 3.3 de l&#8217;interpréteur, et <link xl:href="https://docs.python.org/3/library/venv.html">la manière recommandée</link> de créer un environnement depuis la 3.5).</simpara>
<simpara>Pour créer un nouvel environnement, vous aurez donc besoin:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>D&#8217;une installation de Python - <link xl:href="https://www.python.org/">https://www.python.org/</link></simpara>
</listitem>
<listitem>
<simpara>D&#8217;un terminal - voir le point <link xl:href="../environment/_index.xml#un-terminal">Un terminal</link></simpara>
</listitem>
</orderedlist>
<note>
<simpara>Il existe plusieurs autres modules permettant d&#8217;arriver au même résultat, avec quelques avantages et inconvénients pour chacun d&#8217;entre eux. Le plus prometteur d&#8217;entre eux est <link xl:href="https://python-poetry.org/">Poetry</link>, qui dispose d&#8217;une interface en ligne de commande plus propre et plus moderne que ce que PIP propose.</simpara>
</note>
<simpara>Poetry se propose de gérer le projet au travers d&#8217;un fichier pyproject.toml. TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à ses heures), se place comme alternative aux formats comme JSON, YAML ou INI.</simpara>
<programlisting language="bash" linenumbering="unnumbered">La commande poetry new &lt;project&gt; créera une structure par défaut relativement compréhensible:
$ poetry new django-gecko
$ tree django-gecko/
django-gecko/
├── django_gecko
│ └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
├── __init__.py
└── test_django_gecko.py
2 directories, 5 files</programlisting>
<simpara>Ceci signifie que nous avons directement (et de manière standard):</simpara>
<itemizedlist>
<listitem>
<simpara>Un répertoire django-gecko, qui porte le nom de l&#8217;application que vous venez de créer</simpara>
</listitem>
<listitem>
<simpara>Un répertoires tests, libellé selon les standards de pytest</simpara>
</listitem>
<listitem>
<simpara>Un fichier README.rst (qui ne contient encore rien)</simpara>
</listitem>
<listitem>
<simpara>Un fichier pyproject.toml, qui contient ceci:</simpara>
</listitem>
</itemizedlist>
<programlisting language="toml" linenumbering="unnumbered">[tool.poetry]
name = "django-gecko"
version = "0.1.0"
description = ""
authors = ["... &lt;...@grimbox.be&gt;"]
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
[build-system]
requires = ["poetry-core&gt;=1.0.0"]
build-backend = "poetry.core.masonry.api"</programlisting>
<simpara>La commande <literal>poetry init</literal> permet de générer interactivement les fichiers nécessaires à son intégration dans un projet existant.</simpara>
<note>
<simpara>J&#8217;ai pour habitude de conserver mes projets dans un répertoire <literal>~/Sources/</literal> et mes environnements virtuels dans un répertoire <literal>~/.venvs/</literal>.</simpara>
</note>
<simpara>Cette séparation évite que l&#8217;environnement virtuel ne se trouve dans le même répertoire que les sources, ou ne soit accidentellement envoyé vers le système de gestion de versions.
Elle évite également de rendre ce répertoire "visible" - il ne s&#8217;agit au fond que d&#8217;un paramètre de configuration lié uniquement à votre environnement de développement; les environnements virtuels étant disposables, il n&#8217;est pas conseillé de trop les lier au projet qui l&#8217;utilise comme base.
Dans la suite de ce chapitre, je considérerai ces mêmes répertoires, mais n&#8217;hésitez pas à les modifier.</simpara>
<simpara>DANGER: Indépendamment de l&#8217;endroit où vous stockerez le répertoire contenant cet environnement, il est primordial de <emphasis role="strong">ne pas le conserver dans votre dépôt de stockager</emphasis>.
Cela irait à l&#8217;encontre des douze facteurs, cela polluera inutilement vos sources et créera des conflits avec l&#8217;environnement des personnes qui souhaiteraient intervenir sur le projet.</simpara>
<simpara>Pur créer notre répertoire de travail et notre environnement virtuel, exécutez les commandes suivantes:</simpara>
<programlisting language="bash" linenumbering="unnumbered">mkdir ~/.venvs/
python -m venv ~/.venvs/gwift-venv</programlisting>
<simpara>Ceci aura pour effet de créer un nouveau répertoire (<literal>~/.venvs/gwift-env/</literal>), dans lequel vous trouverez une installation complète de l&#8217;interpréteur Python.
Votre environnement virtuel est prêt, il n&#8217;y a plus qu&#8217;à indiquer que nous souhaitons l&#8217;utiliser, grâce à l&#8217;une des commandes suivantes:</simpara>
<programlisting language="bash" linenumbering="unnumbered"># GNU/Linux, macOS
source ~/.venvs/gwift-venv/bin/activate
# MS Windows, avec Cmder
~/.venvs/gwift-venv/Scripts/activate.bat
# Pour les deux
(gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$ <co xml:id="CO2-1"/></programlisting>
<calloutlist>
<callout arearefs="CO2-1">
<para>Le terminal signale que nous sommes bien dans l&#8217;environnement <literal>gwift-env</literal>.</para>
</callout>
</calloutlist>
<simpara>A présent que l&#8217;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 <literal>PATH</literal> propre est définie et utilisée, afin que les librairies Python y soient stockées.
C&#8217;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.</simpara>
<note>
<simpara>Pour les curieux, un environnement virtuel n&#8217;est jamais qu&#8217;un répertoire dans lequel se trouve une installation fraîche de l&#8217;interpréteur, vers laquelle pointe les liens symboliques des binaires. Si vous recherchez l&#8217;emplacement de l&#8217;interpréteur avec la commande <literal>which python</literal>, vous recevrez comme réponse <literal>/home/fred/.venvs/gwift-env/bin/python</literal>.</simpara>
</note>
<simpara>Pour sortir de l&#8217;environnement virtuel, exécutez la commande <literal>deactivate</literal>.
Si vous pensez ne plus en avoir besoin, supprimer le dossier.
Si nécessaire, il suffira d&#8217;en créer un nouveau.</simpara>
<simpara>Pour gérer des versions différentes d&#8217;une même librairie, il nous suffit de jongler avec autant d&#8217;environnements que nécessaires. Une application nécessite une version de Django inférieure à la 2.0 ? On crée un environnement, on l&#8217;active et on installe ce qu&#8217;il faut.</simpara>
<simpara>Cette technique fonctionnera autant pour un poste de développement que sur les serveurs destinés à recevoir notre application.</simpara>
<note>
<simpara>Par la suite, nous considérerons que l&#8217;environnement virtuel est toujours activé, même si <literal>gwift-env</literal> n&#8217;est pas indiqué.</simpara>
</note>
<simpara>a manière recommandée pour la gestion des dépendances consiste à les épingler dans un fichier requirements.txt, placé à la racine du projet. Ce fichier reprend, ligne par ligne, chaque dépendance et la version nécessaire. Cet épinglage est cependant relativement basique, dans la mesure où les opérateurs disponibles sont ==, &#8656; et &gt;=.</simpara>
<simpara>Poetry propose un épinglage basé sur SemVer. Les contraintes qui peuvent être appliquées aux dépendances sont plus touffues que ce que proposent pip -r, avec la présence du curseur ^, qui ne modifiera pas le nombre différent de zéro le plus à gauche:</simpara>
<literallayout class="monospaced">^1.2.3 (où le nombre en question est 1) pourra proposer une mise à jour jusqu'à la version juste avant la version 2.0.0
^0.2.3 pourra être mise à jour jusqu'à la version juste avant 0.3.0.
...</literallayout>
<simpara>L&#8217;avantage est donc que l&#8217;on spécifie une version majeure - mineure - patchée, et que l&#8217;on pourra spécifier accepter toute mise à jour jusqu&#8217;à la prochaine version majeure - mineure patchée (non incluse 😉).</simpara>
<simpara>Une bonne pratique consiste également, tout comme pour npm, à intégrer le fichier de lock (poetry.lock) dans le dépôt de sources: de cette manière, seules les dépendances testées (et intégrées) seront considérées sur tous les environnements de déploiement.</simpara>
<simpara>Il est alors nécessaire de passer par une action manuelle (poetry update) pour mettre à jour le fichier de verrou, et assurer une mise à jour en sécurité (seules les dépendances testées sont prises en compte) et de qualité (tous les environnements utilisent la même version d&#8217;une dépendance).</simpara>
<simpara>L&#8217;ajout d&#8217;une nouvelle dépendance à un projet se réalise grâce à la commande <literal>poetry add &lt;dep&gt;</literal>:</simpara>
<programlisting language="shell" linenumbering="unnumbered">$ poetry add django
Using version ^3.2.3 for Django
Updating dependencies
Resolving dependencies... (5.1s)
Writing lock file
Package operations: 8 installs, 1 update, 0 removals
• Installing pyparsing (2.4.7)
• Installing attrs (21.2.0)
• Installing more-itertools (8.8.0)
• Installing packaging (20.9)
• Installing pluggy (0.13.1)
• Installing py (1.10.0)
• Installing wcwidth (0.2.5)
• Updating django (3.2 -&gt; 3.2.3)
• Installing pytest (5.4.3)</programlisting>
<simpara>Elle est ensuite ajoutée à notre fichier <literal>pyproject.toml</literal>:</simpara>
<programlisting language="toml" linenumbering="unnumbered">[...]
[tool.poetry.dependencies]
python = "^3.9"
Django = "^3.2.3"
[...]</programlisting>
<simpara>Et contrairement à <literal>pip</literal>, pas besoin de savoir s&#8217;il faut pointer vers un fichier (<literal>-r</literal>) ou un dépôt VCS (<literal>-e</literal>), puisque Poetry va tout essayer, [dans un certain ordre](<link xl:href="https://python-poetry.org/docs/cli/#add">https://python-poetry.org/docs/cli/#add</link>).
L&#8217;avantage également (et cela m&#8217;arrive encore souvent, ce qui fait hurler le runner de Gitlab), c&#8217;est qu&#8217;il n&#8217;est plus nécessaire de penser à épingler la dépendance que l&#8217;on vient d&#8217;installer parmi les fichiers de requirements, puisqu&#8217;elles s&#8217;y ajoutent automatiquement grâce à la commande <literal>add</literal>.</simpara>
</section>
<section xml:id="_python_packaging_made_easy">
<title>Python packaging made easy</title>
<simpara>Cette partie dépasse mes compétences et connaissances, dans la mesure où je n&#8217;ai jamais rien packagé ni publié sur [pypi.org](pypi.org).
Ce n&#8217;est pas l&#8217;envie qui manque, mais les idées et la nécessité 😉.
Ceci dit, Poetry propose un ensemble de règles et une préconfiguration qui (doivent) énormément facilite(r) la mise à disposition de librairies sur Pypi - et rien que ça, devrait ouvrir une partie de l&#8217;écosystème.</simpara>
<simpara>Les chapitres 7 et 8 de [Expert Python Programming - Third Edtion](#), écrit par Michal Jaworski et Tarek Ziadé en parlent très bien:</simpara>
<blockquote>
<simpara>Python packaging can be a bit overwhelming at first.
The main reason for that is the confusion about proper tools for creating Python packages.
Anyway, once you create your first package, you will se that this is as hard as it looks.
Also, knowing propre, state-of-the-art packaging helps a lot.</simpara>
</blockquote>
<simpara>En gros, c&#8217;est ardu-au-début-mais-plus-trop-après.
Et c&#8217;est heureusement suivi et documenté par la PyPA (<emphasis role="strong"><link xl:href="https://github.com/pypa">Python Packaging Authority</link></emphasis>).</simpara>
<simpara>Les étapes sont les suivantes:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Utiliser setuptools pour définir les projets et créer les distributions sources,</simpara>
</listitem>
<listitem>
<simpara>Utiliser <emphasis role="strong">wheels</emphasis> pour créer les paquets,</simpara>
</listitem>
<listitem>
<simpara>Passer par <emphasis role="strong">twine</emphasis> pour envoyer ces paquets vers PyPI</simpara>
</listitem>
<listitem>
<simpara>Définir un ensemble d&#8217;actions (voire, de plugins nécessaires - lien avec le VCS, etc.) dans le fichier <literal>setup.py</literal>, et définir les propriétés du projet ou de la librairie dans le fichier <literal>setup.cfg</literal>.</simpara>
</listitem>
</orderedlist>
<simpara>Avec Poetry, deux commandes suffisent (théoriquement - puisque je n&#8217;ai pas essayé 🤪): <literal>poetry build</literal> et <literal>poetry publish</literal>:</simpara>
<programlisting language="shell" linenumbering="unnumbered">$ poetry build
Building geco (0.1.0)
- Building sdist
- Built geco-0.1.0.tar.gz
- Building wheel
- Built geco-0.1.0-py3-none-any.whl
$ tree dist/
dist/
├── geco-0.1.0-py3-none-any.whl
└── geco-0.1.0.tar.gz
0 directories, 2 files</programlisting>
<simpara>Ce qui est quand même 'achement plus simple que d&#8217;appréhender tout un écosystème.</simpara>
</section>
<section xml:id="_gestion_des_dépendances_installation_de_django_et_création_dun_nouveau_projet">
<title>Gestion des dépendances, installation de Django et création d&#8217;un nouveau projet</title>
<simpara>Comme nous en avons déjà discuté, PIP est la solution que nous avons choisie pour la gestion de nos dépendances.
Pour installer une nouvelle librairie, vous pouvez simplement passer par la commande <literal>pip install &lt;my_awesome_library&gt;</literal>.
Dans le cas de Django, et après avoir activé l&#8217;environnement, nous pouvons à présent y installer Django.
Comme expliqué ci-dessus, la librairie restera indépendante du reste du système, et ne polluera aucun autre projet. nous exécuterons donc la commande suivante:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ source ~/.venvs/gwift-env/bin/activate # ou ~/.venvs/gwift-env/Scrips/activate.bat pour Windows.
$ pip install django
Collecting django
Downloading Django-3.1.4
100% |################################|
Installing collected packages: django
Successfully installed django-3.1.4</programlisting>
<important>
<simpara>Ici, la commande <literal>pip install django</literal> récupère la <emphasis role="strong">dernière version connue disponible dans les dépôts <link xl:href="https://pypi.org/">https://pypi.org/</link></emphasis> (sauf si vous en avez définis d&#8217;autres. Mais c&#8217;est hors sujet).
Nous en avons déjà discuté: il est important de bien spécifier la version que vous souhaitez utiliser, sans quoi vous risquez de rencontrer des effets de bord.</simpara>
</important>
<simpara>L&#8217;installation de Django a ajouté un nouvel exécutable: <literal>django-admin</literal>, que l&#8217;on peut utiliser pour créer notre nouvel espace de travail.
Par la suite, nous utiliserons <literal>manage.py</literal>, qui constitue un <emphasis role="strong">wrapper</emphasis> autour de <literal>django-admin</literal>.</simpara>
<simpara>Pour démarrer notre projet, nous lançons <literal>django-admin startproject gwift</literal>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ django-admin startproject gwift</programlisting>
<simpara>Cette action a pour effet de créer un nouveau dossier <literal>gwift</literal>, dans lequel nous trouvons la structure suivante:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ tree gwift
gwift
├── gwift
| |── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py</programlisting>
<simpara>C&#8217;est dans ce répertoire que vont vivre tous les fichiers liés au projet. Le but est de faire en sorte que toutes les opérations (maintenance, déploiement, écriture, tests, &#8230;&#8203;) puissent se faire à partir d&#8217;un seul point d&#8217;entrée.</simpara>
<simpara>L&#8217;utilité de ces fichiers est définie ci-dessous:</simpara>
<itemizedlist>
<listitem>
<simpara><literal>settings.py</literal> contient tous les paramètres globaux à notre projet.</simpara>
</listitem>
<listitem>
<simpara><literal>urls.py</literal> contient les variables de routes, les adresses utilisées et les fonctions vers lesquelles elles pointent.</simpara>
</listitem>
<listitem>
<simpara><literal>manage.py</literal>, pour toutes les commandes de gestion.</simpara>
</listitem>
<listitem>
<simpara><literal>asgi.py</literal> contient la définition de l&#8217;interface <link xl:href="https://en.wikipedia.org/wiki/Asynchronous_Server_Gateway_Interface">ASGI</link>, le protocole pour la passerelle asynchrone entre votre application et le serveur Web.</simpara>
</listitem>
<listitem>
<simpara><literal>wsgi.py</literal> contient la définition de l&#8217;interface <link xl:href="https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface">WSGI</link>, qui permettra à votre serveur Web (Nginx, Apache, &#8230;&#8203;) de faire un pont vers votre projet.</simpara>
</listitem>
</itemizedlist>
<note>
<simpara>Indiquer qu&#8217;il est possible d&#8217;avoir plusieurs structures de dossiers et qu&#8217;il n&#8217;y a pas de "magie" derrière toutes ces commandes.</simpara>
</note>
<simpara>Tant que nous y sommes, nous pouvons ajouter un répertoire dans lequel nous stockerons les dépendances et un fichier README:</simpara>
<programlisting language="bash" linenumbering="unnumbered">(gwift) $ mkdir requirements
(gwift) $ touch README.md
(gwift) $ tree gwift
gwift
├── gwift
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── requirements <co xml:id="CO3-1"/>
├── README.md <co xml:id="CO3-2"/>
└── manage.py</programlisting>
<calloutlist>
<callout arearefs="CO3-1">
<para>Ici</para>
</callout>
<callout arearefs="CO3-2">
<para>Et là</para>
</callout>
</calloutlist>
<simpara>Comme nous venons d&#8217;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 <literal>requirements.txt</literal>.
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 (<literal>requirements</literal>), afin de grouper les dépendances en fonction de leur environnement de destination:</simpara>
<itemizedlist>
<listitem>
<simpara><literal>base.txt</literal></simpara>
</listitem>
<listitem>
<simpara><literal>dev.txt</literal></simpara>
</listitem>
<listitem>
<simpara><literal>production.txt</literal></simpara>
</listitem>
</itemizedlist>
<simpara>Au début de chaque fichier, il suffit d&#8217;ajouter la ligne <literal>-r base.txt</literal>, puis de lancer l&#8217;installation grâce à un <literal>pip install -r &lt;nom du fichier&gt;</literal>.
De cette manière, il est tout à fait acceptable de n&#8217;installer <literal>flake8</literal> et <literal>django-debug-toolbar</literal> qu&#8217;en développement par exemple.
Dans l&#8217;immédiat, nous allons ajouter <literal>django</literal> dans une version strictement inférieure à la version 3.2 dans le fichier <literal>requirements/base.txt</literal>.</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ echo 'django==3.2' &gt; requirements/base.txt
$ echo '-r base.txt' &gt; requirements/prod.txt
$ echo '-r base.txt' &gt; requirements/dev.txt</programlisting>
<important>
<simpara>Prenez directement l&#8217;habitude de spécifier la version ou les versions compatibles: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets.
Pour être sûr et certain le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances.
Entre deux versions d&#8217;une même librairie, des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc. Il suffit de parcourir les pages de <emphasis>Changements incompatibles avec les anciennes versions dans Django</emphasis> <link xl:href="https://docs.djangoproject.com/fr/3.1/releases/3.0/">(par exemple ici pour le passage de la 3.0 à la 3.1)</link> pour réaliser que certaines opérations ne sont pas anodines, et que sans filet de sécurité, c&#8217;est le mur assuré.
Avec les mécanismes d&#8217;intégration continue et de tests unitaires, nous verrons plus loin comment se prémunir d&#8217;un changement inattendu.</simpara>
</important>
</section>
</section>
<section xml:id="_gestion_des_différentes_versions_des_python">
<title>Gestion des différentes versions des Python</title>
<programlisting language="shell" linenumbering="unnumbered">pyenv install 3.10</programlisting>
</section>
<section xml:id="_django">
<title>Django</title>
<simpara>Comme nous l&#8217;avons vu ci-dessus, <literal>django-admin</literal> permet de créer un nouveau projet.
Nous faisons ici une distinction entre un <emphasis role="strong">projet</emphasis> et une <emphasis role="strong">application</emphasis>:</simpara>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">Un projet</emphasis> représente l&#8217;ensemble des applications, paramètres, pages HTML, middlewares, dépendances, etc., qui font que votre code fait ce qu&#8217;il est sensé faire.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">Une application</emphasis> est un contexte d&#8217;exécution, idéalement autonome, d&#8217;une partie du projet.</simpara>
</listitem>
</itemizedlist>
<simpara>Pour <literal>gwift</literal>, nous aurons:</simpara>
<figure>
<title>Django Projet vs Applications</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/django-project-vs-apps-gwift.png"/>
</imageobject>
<textobject><phrase>django project vs apps gwift</phrase></textobject>
</mediaobject>
</figure>
<orderedlist numeration="arabic">
<listitem>
<simpara>une première application pour la gestion des listes de souhaits et des éléments,</simpara>
</listitem>
<listitem>
<simpara>une deuxième application pour la gestion des utilisateurs,</simpara>
</listitem>
<listitem>
<simpara>voire une troisième application qui gérera les partages entre utilisateurs et listes.</simpara>
</listitem>
</orderedlist>
<simpara>Nous voyons également que la gestion des listes de souhaits et éléments aura besoin de la gestion des utilisateurs - elle n&#8217;est pas autonome -, tandis que la gestion des utilisateurs n&#8217;a aucune autre dépendance qu&#8217;elle-même.</simpara>
<simpara>Pour <literal>khana</literal>, nous pourrions avoir quelque chose comme ceci:</simpara>
<figure>
<title>Django Project vs Applications</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/django-project-vs-apps-khana.png"/>
</imageobject>
<textobject><phrase>django project vs apps khana</phrase></textobject>
</mediaobject>
</figure>
<simpara>En rouge, vous pouvez voir quelque chose que nous avons déjà vu: la gestion des utilisateurs et la possibilité qu&#8217;ils auront de communiquer entre eux.
Ceci pourrait être commun aux deux applications.
Nous pouvons clairement visualiser le principe de <emphasis role="strong">contexte</emphasis> pour une application: celle-ci viendra avec son modèle, ses tests, ses vues et son paramétrage et pourrait ainsi être réutilisée dans un autre projet.
C&#8217;est en ça que consistent les <link xl:href="https://www.djangopackages.com/">paquets Django</link> déjà disponibles: ce sont "<emphasis>simplement</emphasis>" de petites applications empaquetées et pouvant être réutilisées dans différents contextes (eg. <link xl:href="https://github.com/tomchristie/django-rest-framework">Django-Rest-Framework</link>, <link xl:href="https://github.com/django-debug-toolbar/django-debug-toolbar">Django-Debug-Toolbar</link>, &#8230;&#8203;).</simpara>
<section xml:id="_manage_py">
<title>manage.py</title>
<simpara>Le fichier <literal>manage.py</literal> que vous trouvez à la racine de votre projet est un <emphasis role="strong">wrapper</emphasis> sur les commandes <literal>django-admin</literal>.
A partir de maintenant, nous n&#8217;utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet:</simpara>
<itemizedlist>
<listitem>
<simpara><literal>manage.py check</literal> pour vérifier (en surface&#8230;&#8203;) que votre projet ne rencontre aucune erreur évidente</simpara>
</listitem>
<listitem>
<simpara><literal>manage.py check --deploy</literal>, pour vérifier (en surface aussi) que l&#8217;application est prête pour un déploiement</simpara>
</listitem>
<listitem>
<simpara><literal>manage.py runserver</literal> pour lancer un serveur de développement</simpara>
</listitem>
<listitem>
<simpara><literal>manage.py test</literal> pour découvrir les tests unitaires disponibles et les lancer.</simpara>
</listitem>
</itemizedlist>
<simpara>La liste complète peut être affichée avec <literal>manage.py help</literal>.
Vous remarquerez que ces commandes sont groupées selon différentes catégories:</simpara>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">auth</emphasis>: création d&#8217;un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">django</emphasis>: vérifier la <emphasis role="strong">compliance</emphasis> du projet, lancer un <emphasis role="strong">shell</emphasis>, <emphasis role="strong">dumper</emphasis> les données de la base, effectuer une migration du schéma, &#8230;&#8203;</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">sessions</emphasis>: suppressions des sessions en cours</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">staticfiles</emphasis>: gestion des fichiers statiques et lancement du serveur de développement.</simpara>
</listitem>
</itemizedlist>
<simpara>Nous verrons plus tard comment ajouter de nouvelles commandes.</simpara>
<simpara>Si nous démarrons la commande <literal>python manage.py runserver</literal>, nous verrons la sortie console suivante:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
[...]
December 15, 2020 - 20:45:07
Django version 3.1.4, using settings 'gwift.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.</programlisting>
<simpara>Si nous nous rendons sur la page <link xl:href="http://127.0.0.1:8000">http://127.0.0.1:8000</link> (ou <link xl:href="http://localhost:8000">http://localhost:8000</link>) comme le propose si gentiment notre (nouveau) meilleur ami, nous verrons ceci:</simpara>
<figure>
<title>python manage.py runserver (Non, ce n&#8217;est pas Challenger)</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/manage-runserver.png"/>
</imageobject>
<textobject><phrase>manage runserver</phrase></textobject>
</mediaobject>
</figure>
<important>
<simpara>Nous avons mis un morceau de la sortie console entre crochet <literal>[&#8230;&#8203;]</literal> ci-dessus, car elle concerne les migrations.
Si vous avez suivi les étapes jusqu&#8217;ici, vous avez également dû voir un message type <literal>You have 18 unapplied migration(s). [&#8230;&#8203;] Run 'python manage.py migrate' to apply them.</literal>
Cela concerne les migrations, et c&#8217;est un point que nous verrons un peu plus tard.</simpara>
</important>
</section>
<section xml:id="_création_dune_nouvelle_application">
<title>Création d&#8217;une nouvelle application</title>
<simpara>Maintenant que nous avons a vu à quoi servait <literal>manage.py</literal>, nous pouvons créer notre nouvelle application grâce à la commande <literal>manage.py startapp &lt;label&gt;</literal>.</simpara>
<simpara>Notre première application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir.
De manière générale, essayez de trouver un nom éloquent, court et qui résume bien ce que fait l&#8217;application.
Pour nous, ce sera donc <literal>wish</literal>.</simpara>
<simpara>C&#8217;est parti pour <literal>manage.py startapp wish</literal>!</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ python manage.py startapp wish</programlisting>
<simpara>Résultat? Django nous a créé un répertoire <literal>wish</literal>, dans lequel nous trouvons les fichiers et dossiers suivants:</simpara>
<itemizedlist>
<listitem>
<simpara><literal>wish/<emphasis>init</emphasis>.py</literal> pour que notre répertoire <literal>wish</literal> soit converti en package Python.</simpara>
</listitem>
<listitem>
<simpara><literal>wish/admin.py</literal> servira à structurer l&#8217;administration de notre application. Chaque information peut être administrée facilement au travers d&#8217;une interface générée à la volée par le framework. Nous y reviendrons par la suite.</simpara>
</listitem>
<listitem>
<simpara><literal>wish/apps.py</literal> qui contient la configuration de l&#8217;application et qui permet notamment de fixer un nom ou un libellé <link xl:href="https://docs.djangoproject.com/en/stable/ref/applications/">https://docs.djangoproject.com/en/stable/ref/applications/</link></simpara>
</listitem>
<listitem>
<simpara><literal>wish/migrations/</literal> est le dossier dans lequel seront stockées toutes les différentes migrations de notre application (= toutes les modifications que nous apporterons aux données que nous souhaiterons manipuler)</simpara>
</listitem>
<listitem>
<simpara><literal>wish/models.py</literal> représentera et structurera nos données, et est intimement lié aux migrations.</simpara>
</listitem>
<listitem>
<simpara><literal>wish/tests.py</literal> pour les tests unitaires.</simpara>
</listitem>
</itemizedlist>
<note>
<simpara>Par soucis de clarté, vous pouvez déplacer ce nouveau répertoire <literal>wish</literal> dans votre répertoire <literal>gwift</literal> existant.
C&#8217;est une forme de convention.</simpara>
</note>
<simpara>La structure de vos répertoires devient celle-ci:</simpara>
<programlisting language="bash" linenumbering="unnumbered">(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── gwift
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   ├── wish <co xml:id="CO4-1"/>
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   └── wsgi.py
├── Makefile
├── manage.py
├── README.md
├── requirements
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
└── tox.ini
5 directories, 22 files</programlisting>
<calloutlist>
<callout arearefs="CO4-1">
<para>Notre application a bien été créée, et nous l&#8217;avons déplacée dans le répertoire <literal>gwift</literal> !</para>
</callout>
</calloutlist>
</section>
<section xml:id="_fonctionement_général">
<title>Fonctionement général</title>
<simpara>Le métier de programmeur est devenu de plus en plus complexe. Il y a 20 ans, nous pouvions nous contenter d&#8217;une simple page PHP dans laquelle nous mixions l&#8217;ensemble des actios à réaliser: requêtes en bases de données, construction de la page, &#8230;&#8203;
La recherche d&#8217;une solution a un problème n&#8217;était pas spécialement plus complexe - dans la mesure où le rendu des enregistrements en direct n&#8217;était finalement qu&#8217;une forme un chouia plus évoluée du <literal>print()</literal> ou des <literal>System.out.println()</literal> - mais c&#8217;était l&#8217;évolutivité des applications qui en prenait un coup: une grosse partie des tâches étaient dupliquées entre les différentes pages, et l&#8217;ajout d&#8217;une nouvelle fonctionnalité était relativement ardue.</simpara>
<simpara>Django (et d&#8217;autres cadriciels) résolvent ce problème en se basant ouvertement sur le principe de <literal>Don&#8217;t repeat yourself</literal> <footnote><simpara>DRY</simpara></footnote>.
Chaque morceau de code ne doit apparaitre qu&#8217;une seule fois, afin de limiter au maximum la redite (et donc, l&#8217;application d&#8217;un même correctif à différents endroits).</simpara>
<simpara>Le chemin parcouru par une requête est expliqué en (petits) détails ci-dessous.</simpara>
<figure>
<title>How it works</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/diagrams/django-how-it-works.png"/>
</imageobject>
<textobject><phrase>django how it works</phrase></textobject>
</mediaobject>
</figure>
<simpara><emphasis role="strong">1. Un utilisateur ou un visiteur souhaite accéder à une URL hébergée et servie par notre application</emphasis>.
Ici, nous prenons l&#8217;exemple de l&#8217;URL fictive <literal><link xl:href="https://gwift/wishes/91827">https://gwift/wishes/91827</link></literal>.
Lorsque cette URL "arrive" dans notre application, son point d&#8217;entrée se trouvera au niveau des fichiers <literal>asgi.py</literal> ou <literal>wsgi.py</literal>. Nous verrons cette partie plus tard, et nous pouvons nous concentrer sur le chemin interne qu&#8217;elle va parcourir.</simpara>
<simpara><emphasis role="strong">Etape 0</emphasis> - La première étape consiste à vérifier que cette URL répond à un schéma que nous avons défini dans le fichier <literal>gwift/urls.py</literal>.</simpara>
<simpara><emphasis role="strong">Etape 1</emphasis> - Si ce n&#8217;est pas le cas, l&#8217;application n&#8217;ira pas plus loin et retournera une erreur à l&#8217;utilisateur.</simpara>
<simpara><emphasis role="strong">Etape 2</emphasis> - Django va parcourir l&#8217;ensemble des <emphasis>patterns</emphasis> présents dans le fichier <literal>urls.py</literal> et s&#8217;arrêtera sur le premier qui correspondra à la requête qu&#8217;il a reçue.
Ce cas est relativement trivial: la requête <literal>/wishes/91827</literal> a une correspondance au niveau de la ligne <literal>path("wishes/&lt;int:wish_id&gt;</literal> dans l&#8217;exemple ci-dessous.
Django va alors appeler la fonction <footnote><simpara>Qui ne sera pas toujours une fonction. Django s&#8217;attend à trouver un <emphasis>callable</emphasis>, c&#8217;est-à-dire n&#8217;importe quel élément qu&#8217;il peut appeler comme une fonction.</simpara></footnote> associée à ce <emphasis>pattern</emphasis>, c&#8217;est-à-dire <literal>wish_details</literal> du module <literal>gwift.views</literal>.</simpara>
<programlisting language="python" linenumbering="unnumbered">from django.contrib import admin
from django.urls import path
from gwift.views import wish_details <co xml:id="CO5-1"/>
urlpatterns = [
path('admin/', admin.site.urls),
path("wishes/&lt;int:wish_id&gt;", wish_details), <co xml:id="CO5-2"/>
]</programlisting>
<calloutlist>
<callout arearefs="CO5-1">
<para>Nous importons la fonction <literal>wish_details</literal> du module <literal>gwift.views</literal></para>
</callout>
<callout arearefs="CO5-2">
<para>Champomy et cotillons! Nous avons une correspondance avec <literal>wishes/details/91827</literal></para>
</callout>
</calloutlist>
<simpara>TODO: En fait, il faudrait quand même s&#8217;occuper du modèle ici.
TODO: et de la mise en place de l&#8217;administration, parce que nous en aurons besoin pour les étapes de déploiement.</simpara>
<simpara><phrase role="line-through">Nous n&#8217;allons pas nous occuper de l&#8217;accès à la base de données pour le moment (nous nous en occuperons dans un prochain chapitre) et nous nous contenterons de remplir un canevas avec un ensemble de données.</phrase></simpara>
<simpara>Le module <literal>gwift.views</literal> qui se trouve dans le fichier <literal>gwift/views.py</literal> peut ressembler à ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered">[...]
from datetime import datetime
def wishes_details(request: HttpRequest, wish_id: int) -&gt; HttpResponse:
context = {
"user_name": "Bond,"
"user_first_name": "James",
"now": datetime.now()
}
return render(
request,
"wish_details.html",
context
)</programlisting>
<simpara>Pour résumer, cette fonction permet:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>De construire un <emphasis>contexte</emphasis>, qui est représenté sous la forme d&#8217;un dictionnaire associant des clés à des valeurs. Les clés sont respectivement <literal>user_name</literal>, <literal>user_first_name</literal> et <literal>now</literal>, tandis que leurs valeurs respectives sont <literal>Bond</literal>, <literal>James</literal> et le <literal>moment présent</literal> <footnote><simpara>Non, pas celui d&#8217;Eckhart Tolle</simpara></footnote>.</simpara>
</listitem>
<listitem>
<simpara>Nous passons ensuite ce dictionnaire à un canevas, <literal>wish_details.html</literal></simpara>
</listitem>
<listitem>
<simpara>L&#8217;application du contexte sur le canevas nous donne un résultat.</simpara>
</listitem>
</orderedlist>
<programlisting language="html" linenumbering="unnumbered">&lt;!-- fichier wish_details.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Page title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;👤 Hi!&lt;/h1&gt;
&lt;p&gt;My name is {{ user_name }}. {{ user_first_name }} {{ user_name }}.&lt;/p&gt;
&lt;p&gt;This page was generated at {{ now }}&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</programlisting>
<simpara>Après application de notre contexte sur ce template, nous obtiendrons ce document, qui sera renvoyé au navigateur de l&#8217;utilisateur qui aura fait la requête initiale:</simpara>
<programlisting language="html" linenumbering="unnumbered">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Page title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;👤 Hi!&lt;/h1&gt;
&lt;p&gt;My name is Bond. James Bond.&lt;/p&gt;
&lt;p&gt;This page was generated at 2027-03-19 19:47:38&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</programlisting>
<figure>
<title>Résultat</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/django-first-template.png"/>
</imageobject>
<textobject><phrase>django first template</phrase></textobject>
</mediaobject>
</figure>
</section>
<section xml:id="_12_facteurs_et_configuration_globale">
<title>12 facteurs et configuration globale</title>
<simpara>&#8594; Faire le lien avec les settings
&#8594; Faire le lien avec les douze facteurs
&#8594; Construction du fichier setup.cfg</simpara>
</section>
<section xml:id="_setup_cfg">
<title>setup.cfg</title>
<simpara>(Repris de cookie-cutter-django)</simpara>
<programlisting language="ini" linenumbering="unnumbered">[flake8]
max-line-length = 120
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[pycodestyle]
max-line-length = 120
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[mypy]
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
plugins = mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = config.settings.test
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True
[coverage:run]
include = khana/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin</programlisting>
</section>
</section>
<section xml:id="_structure_finale_de_notre_environnement">
<title>Structure finale de notre environnement</title>
<simpara>Nous avons donc la structure finale pour notre environnement de travail:</simpara>
<programlisting language="bash" linenumbering="unnumbered">(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── gwift
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   ├── wish <co xml:id="CO6-1"/>
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   └── wsgi.py
├── Makefile
├── manage.py
├── README.md
├── requirements
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
└── tox.ini</programlisting>
</section>
<section xml:id="_cookie_cutter">
<title>Cookie cutter</title>
<simpara>Pfiou! Ca en fait des commandes et du boulot pour "juste" démarrer un nouveau projet, non? Sachant qu&#8217;en plus, nous avons dû modifier des fichiers, déplacer des dossiers, ajouter des dépendances, configurer une base de données, &#8230;&#8203;</simpara>
<simpara>Bonne nouvelle! Il existe des générateurs, permettant de démarrer rapidement un nouveau projet sans (trop) se prendre la tête. Le plus connu (et le plus personnalisable) est <link xl:href="https://cookiecutter.readthedocs.io/">Cookie-Cutter</link>, qui se base sur des canevas <emphasis>type <link xl:href="https://pypi.org/project/Jinja2/">Jinja2</link></emphasis>, pour créer une arborescence de dossiers et fichiers conformes à votre manière de travailler. Et si vous avez la flemme de créer votre propre canevas, vous pouvez utiliser <link xl:href="https://cookiecutter-django.readthedocs.io">ceux qui existent déjà</link>.</simpara>
<simpara>Pour démarrer, créez un environnement virtuel (comme d&#8217;habitude):</simpara>
<programlisting language="bash" linenumbering="unnumbered">λ python -m venv .venvs\cookie-cutter-khana
λ .venvs\cookie-cutter-khana\Scripts\activate.bat
(cookie-cutter-khana) λ pip install cookiecutter
Collecting cookiecutter
[...]
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 arrow-0.17.0 binaryornot-0.4.4 certifi-2020.12.5 chardet-4.0.0 click-7.1.2 cookiecutter-1.7.2 idna-2.10 jinja2-time-0.2.0 poyo-0.5.0 python-dateutil-2.8.1 python-slugify-4.0.1 requests-2.25.1 six-1.15.0 text-unidecode-1.3 urllib3-1.26.2
(cookie-cutter-khana) λ cookiecutter https://github.com/pydanny/cookiecutter-django
[...]
[SUCCESS]: Project initialized, keep up the good work!</programlisting>
<simpara>Si vous explorez les différents fichiers, vous trouverez beaucoup de similitudes avec la configuration que nous vous proposions ci-dessus.
En fonction de votre expérience, vous serez tenté de modifier certains paramètres, pour faire correspondre ces sources avec votre utilisation ou vos habitudes.</simpara>
<note>
<simpara>Il est aussi possible d&#8217;utiliser l&#8217;argument <literal>--template</literal>, suivie d&#8217;un argument reprenant le nom de votre projet (<literal>&lt;my_project&gt;</literal>), lors de l&#8217;initialisation d&#8217;un projet avec la commande <literal>startproject</literal> de <literal>django-admin</literal>, afin de calquer votre arborescence sur un projet existant.
La <link xl:href="https://docs.djangoproject.com/en/stable/ref/django-admin/#startproject">documentation</link> à ce sujet est assez complète.</simpara>
</note>
<programlisting language="bash" linenumbering="unnumbered">django-admin.py startproject --template=https://[...].zip &lt;my_project&gt;</programlisting>
</section>
</chapter>
</part>
<part xml:id="_principes_fondamentaux">
<title>Principes fondamentaux</title>
<partintro>
<simpara>Dans ce chapitre, nous allons parler de plusieurs concepts fondamentaux au développement rapide d&#8217;une application utilisant Django.
Nous parlerons de modélisation, de métamodèle, de migrations, d&#8217;administration auto-générée, de traductions et de cycle de vie des données.</simpara>
<simpara>Django est un framework Web qui propose une très bonne intégration des composants et une flexibilité bien pensée: chacun des composants permet de définir son contenu de manière poussée, en respectant des contraintes logiques et faciles à retenir, et en gérant ses dépendances de manière autonome.
Pour un néophyte, la courbe d&#8217;apprentissage sera relativement ardue: à côté de concepts clés de Django, il conviendra également d&#8217;assimiler correctement les structures de données du langage Python, le cycle de vie des requêtes HTTP et le B.A-BA des principes de sécurité.</simpara>
<simpara>En restant dans les sentiers battus, votre projet suivra un patron de conception dérivé du modèle <literal>MVC</literal> (Modèle-Vue-Controleur), où la variante concerne les termes utilisés: Django les nomme respectivement Modèle-Template-Vue et leur contexte d&#8217;utilisation.
Dans un <emphasis role="strong">pattern</emphasis> MVC classique, la traduction immédiate du <emphasis role="strong">contrôleur</emphasis> est une <emphasis role="strong">vue</emphasis>.
Et comme nous le verrons par la suite, la <emphasis role="strong">vue</emphasis> est en fait le <emphasis role="strong">template</emphasis>.
La principale différence avec un modèle MVC concerne le fait que la vue ne s&#8217;occupe pas du routage des URLs; ce point est réalisé par un autre composant, interne au framework, graĉe aux différentes routes définies dans les fichiers <literal>urls.py</literal>.</simpara>
<itemizedlist>
<listitem>
<simpara>Le <emphasis role="strong">modèle</emphasis> (<literal>models.py</literal>) fait le lien avec la base de données et permet de définir les champs et leur type à associer à une table. <emphasis>Grosso modo</emphasis>*, une table SQL correspondra à une classe d&#8217;un modèle Django.</simpara>
</listitem>
<listitem>
<simpara>La <emphasis role="strong">vue</emphasis> (<literal>views.py</literal>), qui joue le rôle de contrôleur: <emphasis>a priori</emphasis>, 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&#8217;affichage d&#8217;une page. En d&#8217;autres mots, la vue sert de pont entre les données gérées par la base et l&#8217;interface utilisateur.</simpara>
</listitem>
<listitem>
<simpara>Le <emphasis role="strong">template</emphasis>, qui s&#8217;occupe de la mise en forme: c&#8217;est le composant qui s&#8217;occupe de transformer les données en un affichage compréhensible (avec l&#8217;aide du navigateur) pour l&#8217;utilisateur.</simpara>
</listitem>
</itemizedlist>
<simpara>Pour reprendre une partie du schéma précédent, lorsqu&#8217;une requête est émise par un utilisateur, la première étape va consister à trouver une <emphasis>route</emphasis> qui correspond à cette requête, c&#8217;est à dire à trouver la correspondance entre l&#8217;URL qui est demandée par l&#8217;utilisateur et la fonction du langage qui sera exécutée pour fournir le résultat attendu.
Cette fonction correspond au <emphasis role="strong">contrôleur</emphasis> et s&#8217;occupera de construire le <emphasis role="strong">modèle</emphasis> correspondant.</simpara>
</partintro>
<chapter xml:id="_modélisation">
<title>Modélisation</title>
<simpara>Ce chapitre aborde la modélisation des objets et les options qui y sont liées.</simpara>
<simpara>Avec Django, la modélisation est en lien direct avec la conception et le stockage, sous forme d&#8217;une base de données relationnelle, et la manière dont ces données s&#8217;agencent et communiquent entre elles.
Cette modélisation va ériger les premières pierres de votre édifice</simpara>
<blockquote>
<attribution>
Aurélie Jean
<citetitle>De l'autre côté de la machine</citetitle>
</attribution>
<simpara><emphasis>Le modèle n&#8217;est qu&#8217;une grande hypothèque.
Il se base sur des choix conscients et inconscients, et dans chacun de ces choix se cachent nos propres perceptions qui résultent de qui nous sommes, de nos connaissances, de nos profils scientifiques et de tant d&#8217;autres choses.</emphasis></simpara>
</blockquote>
<simpara>Comme expliqué par Aurélie Jean cite:[other_side], "<emphasis>toute modélisation reste une approximation de la réalité</emphasis>".
Plus tard dans ce chapitre, nous expliquerons les bonnes pratiques à suivre pour faire évoluer ces biais.</simpara>
<simpara>Django utilise un paradigme de persistence des données de type <link xl:href="https://fr.wikipedia.org/wiki/Mapping_objet-relationnel">ORM</link> - c&#8217;est-à-dire que chaque type d&#8217;objet manipulé peut s&#8217;apparenter à une table SQL, tout en respectant une approche propre à la programmation orientée object.
Plus spécifiquement, l&#8217;ORM de Django suit le patron de conception <link xl:href="https://en.wikipedia.org/wiki/Active_record_pattern">Active Records</link>, comme le font par exemple <link xl:href="https://rubyonrails.org/">Rails</link> pour Ruby ou <link xl:href="https://docs.microsoft.com/fr-fr/ef/">EntityFramework</link> pour .Net.</simpara>
<simpara>Le modèle de données de Django est sans doute la (seule ?) partie qui soit tellement couplée au framework qu&#8217;un changement à ce niveau nécessitera une refonte complète de beaucoup d&#8217;autres briques de vos applications; là où un pattern de type <link xl:href="https://www.martinfowler.com/eaaCatalog/repository.html">Repository</link> permettrait justement de découpler le modèle des données de l&#8217;accès à ces mêmes données, un pattern Active Record lie de manière extrêmement forte le modèle à sa persistence.
Architecturalement, c&#8217;est sans doute la plus grosse faiblesse de Django, à tel point que <emphasis role="strong">ne pas utiliser cette brique de fonctionnalités</emphasis> peut remettre en question le choix du framework.</simpara>
<simpara>Conceptuellement, c&#8217;est pourtant la manière de faire qui permettra d&#8217;avoir quelque chose à présenter très rapidement: à partir du moment où vous aurez un modèle de données, vous aurez accès, grâce à cet ORM à:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Des migrations de données et la possibilité de faire évoluer votre modèle,</simpara>
</listitem>
<listitem>
<simpara>Une abstraction entre votre modélisation et la manière dont les données sont représentées <emphasis>via</emphasis> un moteur de base de données relationnelles,</simpara>
</listitem>
<listitem>
<simpara>Une interface d&#8217;administration auto-générée</simpara>
</listitem>
<listitem>
<simpara>Un mécanisme de formulaires HTML qui soit complet, pratique à utiliser, orienté objet et facile à faire évoluer,</simpara>
</listitem>
<listitem>
<simpara>Une définition des notions d&#8217;héritage (tout en restant dans une forme d&#8217;héritage simple).</simpara>
</listitem>
</orderedlist>
<simpara>Comme tout ceci reste au niveau du code, cela suit également la méthodologie des douze facteurs, concernant la minimisation des divergences entre environnements d&#8217;exécution: comme tout se trouve au niveau du code, il n&#8217;est plus nécessaire d&#8217;avoir un DBA qui doive démarrer un script sur un serveur au moment de la mise à jour, de recevoir une release note de 512 pages en PDF reprenant les modifications ou de nécessiter l&#8217;intervention de trois équipes différentes lors d&#8217;une modification majeure du code.
Déployer une nouvelle instance de l&#8217;application pourra être réalisé directement à partir d&#8217;une seule et même commande.</simpara>
<section xml:id="_active_records">
<title>Active Records</title>
<simpara>Il est important de noter que l&#8217;implémentation d&#8217;Active Records reste une forme hybride entre une structure de données brutes et une classe:</simpara>
<itemizedlist>
<listitem>
<simpara>Une classe va exposer ses données derrière une forme d&#8217;abstraction et n&#8217;exposer que les fonctions qui opèrent sur ces données,</simpara>
</listitem>
<listitem>
<simpara>Une structure de données ne va exposer que ses champs et propriétés, et ne va pas avoir de functions significatives.</simpara>
</listitem>
</itemizedlist>
<simpara>L&#8217;exemple ci-dessous présente trois structure de données, qui exposent chacune leurs propres champs:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Square:
def __init__(self, top_left, side):
self.top_left = top_left
self.side = side
class Rectangle:
def __init__(self, top_left, height, width):
self.top_left = top_left
self.height = height
self.width = width
class Circle:
def __init__(self, center, radius):
self.center = center
self.radius = radius</programlisting>
<simpara>Si nous souhaitons ajouter une fonctionnalité permettant de calculer l&#8217;aire pour chacune de ces structures, nous aurons deux possibilités:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Soit ajouter une classe de <emphasis>visite</emphasis> qui ajoute cette fonction de calcul d&#8217;aire</simpara>
</listitem>
<listitem>
<simpara>Soit modifier notre modèle pour que chaque structure hérite d&#8217;une classe de type <literal>Shape</literal>, qui implémentera elle-même ce calcul d&#8217;aire.</simpara>
</listitem>
</orderedlist>
<simpara>Dans le premier cas, nous pouvons procéder de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Geometry:
PI = 3.141592653589793
def area(self, shape):
if isinstance(shape, Square):
return shape.side * shape.side
if isinstance(shape, Rectangle):
return shape.height * shape.width
if isinstance(shape, Circle):
return PI * shape.radius**2
raise NoSuchShapeException()</programlisting>
<simpara>Dans le second cas, l&#8217;implémentation pourrait évoluer de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Shape:
def area(self):
pass
class Square(Shape):
def __init__(self, top_left, side):
self.__top_left = top_left
self.__side = side
def area(self):
return self.__side * self.__side
class Rectangle(Shape):
def __init__(self, top_left, height, width):
self.__top_left = top_left
self.__height = height
self.__width = width
def area(self):
return self.__height * self.__width
class Circle(Shape):
def __init__(self, center, radius):
self.__center = center
self.__radius = radius
def area(self):
PI = 3.141592653589793
return PI * self.__radius**2</programlisting>
<simpara>Une structure de données peut être rendue abstraite au travers des notions de programmation orientée objet.</simpara>
<simpara>Dans l&#8217;exemple géométrique ci-dessus, repris de cite:[clean_code(95-97)], l&#8217;accessibilité des champs devient restreinte, tandis que la fonction <literal>area()</literal> bascule comme méthode d&#8217;instance plutôt que de l&#8217;isoler au niveau d&#8217;un visiteur.
Nous ajoutons une abstraction au niveau des formes grâce à un héritage sur la classe <literal>Shape</literal>; indépendamment de ce que nous manipulerons, nous aurons la possibilité de calculer son aire.</simpara>
<simpara>Une structure de données permet de facilement gérer des champs et des propriétés, tandis qu&#8217;une classe gère et facilite l&#8217;ajout de fonctions et de méthodes.</simpara>
<simpara>Le problème d&#8217;Active Records est que chaque classe s&#8217;apparente à une table SQL et revient donc à gérer des <emphasis>DTO</emphasis> ou <emphasis>Data Transfer Object</emphasis>, c&#8217;est-à-dire des objets de correspondance pure et simple entre les champs de la base de données et les propriétés de la programmation orientée objet, c&#8217;est-à-dire également des classes sans fonctions.
Or, chaque classe a également la possibilité d&#8217;exposer des possibilités d&#8217;interactions au niveau de la persistence, en <link xl:href="https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.save">enregistrant ses propres données</link> ou en en autorisant leur <link xl:href="https://docs.djangoproject.com/en/stable/ref/models/instances/#deleting-objects">suppression</link>.
Nous arrivons alors à un modèle hybride, mélangeant des structures de données et des classes d&#8217;abstraction, ce qui restera parfaitement viable tant que l&#8217;on garde ces principes en tête et que l&#8217;on se prépare à une éventuelle réécriture du code.</simpara>
<simpara>Lors de l&#8217;analyse d&#8217;une classe de modèle, nous pouvons voir que Django exige un héritage de la classe <literal>django.db.models.Model</literal>.
Nous pouvons regarder les propriétés définies dans cette classe en analysant le fichier <literal>lib\site-packages\django\models\base.py</literal>.
Outre que <literal>models.Model</literal> hérite de <literal>ModelBase</literal> au travers de <link xl:href="https://pypi.python.org/pypi/six">six</link> pour la rétrocompatibilité vers Python 2.7, cet héritage apporte notamment les fonctions <literal>save()</literal>, <literal>clean()</literal>, <literal>delete()</literal>, &#8230;&#8203;
En résumé, toutes les méthodes qui font qu&#8217;une instance sait <emphasis role="strong">comment</emphasis> interagir avec la base de données.</simpara>
</section>
<section xml:id="_types_de_champs_relations_et_clés_étrangères">
<title>Types de champs, relations et clés étrangères</title>
<simpara>Nous l&#8217;avons vu plus tôt, Python est un langage dynamique et fortement typé.
Django, de son côté, ajoute une couche de typage statique exigé par le lien sous-jacent avec le moteur de base de données relationnelle.
Dans le domaine des bases de données relationnelles, un point d&#8217;attention est de toujours disposer d&#8217;une clé primaire pour nos enregistrements.
Si aucune clé primaire n&#8217;est spécifiée, Django s&#8217;occupera d&#8217;en ajouter une automatiquement et la nommera (par convention) <literal>id</literal>.
Elle sera ainsi accessible autant par cette propriété que par la propriété <literal>pk</literal>.</simpara>
<simpara>Chaque champ du modèle est donc typé et lié, soit à une primitive, soit à une autre instance au travers de sa clé d&#8217;identification.</simpara>
<simpara>Grâce à toutes ces informations, nous sommes en mesure de représenter facilement des livres liés à des catégories:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)</programlisting>
<simpara>Par défaut, et si aucune propriété ne dispose d&#8217;un attribut <literal>primary_key=True</literal>, Django s&#8217;occupera d&#8217;ajouter un champ <literal>id</literal> grâce à son héritage de la classe <literal>models.Model</literal>.
Les autres champs nous permettent d&#8217;identifier une catégorie (<literal>Category</literal>) par un nom (<literal>name</literal>), tandis qu&#8217;un livre (<literal>Book</literal>) le sera par ses propriétés <literal>title</literal> et une clé de relation vers une catégorie.
Un livre est donc lié à une catégorie, tandis qu&#8217;une catégorie est associée à plusieurs livres.</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="diagrams/books-foreign-keys-example.drawio.png"/>
</imageobject>
<textobject><phrase>books foreign keys example.drawio</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>A présent que notre structure dispose de sa modélisation, il nous faut informer le moteur de base de données de créer la structure correspondance:</simpara>
<programlisting language="shell" linenumbering="unnumbered">$ python manage.py makemigrations
Migrations for 'library':
library/migrations/0001_initial.py
- Create model Category
- Create model Book</programlisting>
<simpara>Cette étape créera un fichier différentiel, explicitant les modifications à appliquer à la structure de données pour rester en corrélation avec la modélisation de notre application.</simpara>
<simpara>Nous pouvons écrire un premier code d&#8217;initialisation de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered">from library.models import Book, Category
movies = Category.objects.create(name="Adaptations au cinéma")
medieval = Category.objects.create(name="Médiéval-Fantastique")
science_fiction = Category.objects.create(name="Sciences-fiction")
computers = Category.objects.create(name="Sciences Informatiques")
books = {
"Harry Potter": movies,
"The Great Gatsby": movies,
"Dune": science_fiction,
"H2G2": science_fiction,
"Ender's Game": science_fiction,
"Le seigneur des anneaux": medieval,
"L'Assassin Royal", medieval,
"Clean code": computers,
"Designing Data-Intensive Applications": computers
}
for book_title, category in books.items:
Book.objects.create(name=book_title, category=category)</programlisting>
<simpara>Nous nous rendons rapidement compte qu&#8217;un livre peut appartenir à plusieurs catégories:</simpara>
<itemizedlist>
<listitem>
<simpara><emphasis>Dune</emphasis> a été adapté au cinéma en 1973 et en 2021, de même que <emphasis>Le Seigneur des Anneaux</emphasis>. Ces deux titres (au moins) peuvent appartenir à deux catégories distinctes.</simpara>
</listitem>
<listitem>
<simpara>Pour <emphasis>The Great Gatsby</emphasis>, c&#8217;est l&#8217;inverse: nous l&#8217;avons initialement classé comme film, mais le livre existe depuis 1925.</simpara>
</listitem>
<listitem>
<simpara>Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie spéciale "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga <emphasis>Harry Potter</emphasis> et ses dérivés.</simpara>
</listitem>
</itemizedlist>
<simpara>En clair, notre modèle n&#8217;est pas adapté, et nous devons le modifier pour qu&#8217;une occurrence puisse être liée à plusieurs catégories.
Au lieu d&#8217;utiliser un champ de type <literal>ForeignKey</literal>, nous utiliserons un champ de type <literal>ManyToMany</literal>, c&#8217;est-à-dire qu&#8217;à présent, un livre pourra être lié à plusieurs catégories, et qu&#8217;inversément, une même catégorie pourra être liée à plusieurs livres.</simpara>
<programlisting language="python" linenumbering="unnumbered">class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category, on_delete=models.CASCADE)</programlisting>
<simpara>Notre code d&#8217;initialisation reste par contre identique: Django s&#8217;occupe parfaitement de gérer la transition.</simpara>
<section xml:id="_accès_aux_relations">
<title>Accès aux relations</title>
<programlisting language="python" linenumbering="unnumbered"># wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist)</programlisting>
<simpara>Depuis le code, à partir de l&#8217;instance de la classe <literal>Item</literal>, on peut donc accéder à la liste en appelant la propriété <literal>wishlist</literal> de notre instance. <emphasis role="strong">A contrario</emphasis>, depuis une instance de type <literal>Wishlist</literal>, on peut accéder à tous les éléments liés grâce à <literal>&lt;nom de la propriété&gt;_set</literal>; ici <literal>item_set</literal>.</simpara>
<simpara>Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l&#8217;attribut <literal>related_name</literal> afin de nommer la relation inverse.</simpara>
<programlisting language="python" linenumbering="unnumbered"># wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist, related_name='items')</programlisting>
<note>
<simpara>Si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse.
Pour palier à ce problème, nous fixons une valeur à l&#8217;attribut <literal>related_name</literal>.
Par facilité (et par conventions), prenez l&#8217;habitude de toujours ajouter cet attribut: votre modèle gagnera en cohérence et en lisibilité.
Si cette relation inverse n&#8217;est pas nécessaire, il est possible de l&#8217;indiquer (par convention) au travers de l&#8217;attribut <literal>related_name="+"</literal>.</simpara>
</note>
<simpara>A partir de maintenant, nous pouvons accéder à nos propriétés de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered"># python manage.py shell
&gt;&gt;&gt; from wish.models import Wishlist, Item
&gt;&gt;&gt; wishlist = Wishlist.create('Liste de test', 'description')
&gt;&gt;&gt; item = Item.create('Element de test', 'description', w)
&gt;&gt;&gt;
&gt;&gt;&gt; item.wishlist
&lt;Wishlist: Wishlist object&gt;
&gt;&gt;&gt;
&gt;&gt;&gt; wishlist.items.all()
[&lt;Item: Item object&gt;]</programlisting>
</section>
<section xml:id="_n1_queries">
<title>N+1 Queries</title>
</section>
</section>
<section xml:id="_unicité">
<title>Unicité</title>
</section>
<section xml:id="_indices">
<title>Indices</title>
<section xml:id="_conclusions">
<title>Conclusions</title>
<simpara>Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (<emphasis role="strong">ForeignKey</emphasis>) d&#8217;une classe A vers une classe B.
Pour représenter d&#8217;autres types de relations, il existe également les champs de type <emphasis role="strong">ManyToManyField</emphasis>, afin de représenter une relation N-N.
Il existe également un type de champ spécial pour les clés étrangères, qui est le Les champs de type <emphasis role="strong">OneToOneField</emphasis>, pour représenter une relation 1-1.</simpara>
</section>
<section xml:id="_metamodèle_et_introspection">
<title>Metamodèle et introspection</title>
<simpara>Comme chaque classe héritant de <literal>models.Model</literal> possède une propriété <literal>objects</literal>. Comme on l&#8217;a vu dans la section <emphasis role="strong">Jouons un peu avec la console</emphasis>, cette propriété permet d&#8217;accéder aux objects persistants dans la base de données, au travers d&#8217;un <literal>ModelManager</literal>.</simpara>
<simpara>En plus de cela, il faut bien tenir compte des propriétés <literal>Meta</literal> de la classe: si elle contient déjà un ordre par défaut, celui-ci sera pris en compte pour l&#8217;ensemble des requêtes effectuées sur cette classe.</simpara>
<programlisting language="python" linenumbering="unnumbered">class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',) <co xml:id="CO6-2"/></programlisting>
<calloutlist>
<callout arearefs="CO6-1 CO6-2">
<para>Nous définissons un ordre par défaut, directement au niveau du modèle. Cela ne signifie pas qu&#8217;il ne sera pas possible de modifier cet ordre (la méthode <literal>order_by</literal> existe et peut être chaînée à n&#8217;importe quel <emphasis>queryset</emphasis>). D&#8217;où l&#8217;intérêt de tester ce type de comportement, dans la mesure où un <literal>top 1</literal> dans votre code pourrait être modifié simplement par cette petite information.</para>
</callout>
</calloutlist>
<simpara>Pour sélectionner un objet au pif : <literal>return Category.objects.order_by("?").first()</literal></simpara>
<simpara>Les propriétés de la classe Meta les plus utiles sont les suivates:</simpara>
<itemizedlist>
<listitem>
<simpara><literal>ordering</literal> pour spécifier un ordre de récupération spécifique.</simpara>
</listitem>
<listitem>
<simpara><literal>verbose_name</literal> pour indiquer le nom à utiliser au singulier pour définir votre classe</simpara>
</listitem>
<listitem>
<simpara><literal>verbose_name_plural</literal>, pour le pluriel.</simpara>
</listitem>
<listitem>
<simpara><literal>contraints</literal> (Voir <link xl:href="https://girlthatlovestocode.com/django-model">ici</link>-), par exemple</simpara>
</listitem>
</itemizedlist>
<programlisting language="python" linenumbering="unnumbered"> constraints = [ # constraints added
models.CheckConstraint(check=models.Q(year_born__lte=datetime.date.today().year-18), name='will_be_of_age'),
]</programlisting>
</section>
<section xml:id="_choix">
<title>Choix</title>
<simpara>Voir <link xl:href="https://girlthatlovestocode.com/django-model">ici</link></simpara>
<programlisting language="python" linenumbering="unnumbered">class Runner(models.Model):
# this is new:
class Zone(models.IntegerChoices):
ZONE_1 = 1, 'Less than 3.10'
ZONE_2 = 2, 'Less than 3.25'
ZONE_3 = 3, 'Less than 3.45'
ZONE_4 = 4, 'Less than 4 hours'
ZONE_5 = 5, 'More than 4 hours'
name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField()
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?") # this is new</programlisting>
</section>
<section xml:id="_validateurs">
<title>Validateurs</title>
</section>
<section xml:id="_constructeurs">
<title>Constructeurs</title>
<simpara>Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode <literal><emphasis>init</emphasis></literal>: créez plutôt une méthode static de type <literal>create()</literal>, en y associant les paramètres obligatoires ou souhaités:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Wishlist(models.Model):
@staticmethod
def create(name, description):
w = Wishlist()
w.name = name
w.description = description
w.save()
return w
class Item(models.Model):
@staticmethod
def create(name, description, wishlist):
i = Item()
i.name = name
i.description = description
i.wishlist = wishlist
i.save()
return i</programlisting>
<simpara>Mieux encore: on pourrait passer par un <literal>ModelManager</literal> pour limiter le couplage;
l&#8217;accès à une information stockée en base de données ne se ferait dès lors qu&#8217;au travers
de cette instance et pas directement au travers du modèle. De cette manière, on limite le
couplage des classes et on centralise l&#8217;accès.</simpara>
<programlisting language="python" linenumbering="unnumbered">class ItemManager(...):
(de mémoire, je ne sais plus exactement :-))</programlisting>
</section>
</section>
<section xml:id="_conclusion_2">
<title>Conclusion</title>
<simpara>Le modèle proposé par Django est un composant extrêmement performant, mais fort couplé avec le coeur du framework.
Si tous les composants peuvent être échangés avec quelques manipulations, le cas du modèle sera plus difficile à interchanger.</simpara>
<simpara>A côté de cela, il permet énormément de choses, et vous fera gagner un temps précieux, tant en rapidité d&#8217;essais/erreurs, que de preuves de concept.</simpara>
<simpara>Une possibilité peut également être de cantonner Django à un framework</simpara>
</section>
<section xml:id="_querysets_managers">
<title>Querysets &amp; managers</title>
<itemizedlist>
<listitem>
<simpara><link xl:href="http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm">http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator">https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="http://blog.etianen.com/blog/2013/06/08/django-querysets/">http://blog.etianen.com/blog/2013/06/08/django-querysets/</link></simpara>
</listitem>
</itemizedlist>
<simpara>L&#8217;ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants:</simpara>
<itemizedlist>
<listitem>
<simpara>Les <literal>managers</literal>, qui consistent en un point d&#8217;entrée pour accéder aux objets persistants</simpara>
</listitem>
<listitem>
<simpara>Les <literal>querysets</literal>, qui permettent de filtrer des ensembles ou sous-ensemble d&#8217;objets. Les querysets peuvent s&#8217;imbriquer, pour ajouter
d&#8217;autres filtres à des filtres existants, et fonctionnent comme un super jeu d&#8217;abstraction pour accéder à nos données (persistentes).</simpara>
</listitem>
</itemizedlist>
<simpara>Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut <literal>objects</literal>, qui correspond
à un manager (ou un gestionnaire, si vous préférez).
Ce gestionnaire constitue l&#8217;interface par laquelle vous accéderez à la base de données. Mais pour cela, vous aurez aussi besoin d&#8217;appliquer certains requêtes ou filtres. Et pour cela, vous aurez besoin des <literal>querysets</literal>, qui consistent en des &#8230;&#8203; ensembles de requêtes :-).</simpara>
<simpara>Si on veut connaître la requête SQL sous-jacente à l&#8217;exécution du queryset, il suffit d&#8217;appeler la fonction str() sur la propriété <literal>query</literal>:</simpara>
<programlisting language="python" linenumbering="unnumbered">queryset = Wishlist.objects.all()
print(queryset.query)</programlisting>
<simpara>Conditions AND et OR sur un queryset</simpara>
<literallayout class="monospaced">Pour un `AND`, il suffit de chaîner les conditions. ** trouver un exemple ici ** :-)</literallayout>
<literallayout class="monospaced">Mais en gros : bidule.objects.filter(condition1, condition2)</literallayout>
<literallayout class="monospaced">Il existe deux autres options : combiner deux querysets avec l'opérateur `&amp;` ou combiner des Q objects avec ce même opérateur.</literallayout>
<simpara>Soit encore combiner des filtres:</simpara>
<programlisting language="python" linenumbering="unnumbered">from core.models import Wish
Wish.objects <co xml:id="CO7-1"/>
Wish.objects.filter(name__icontains="test").filter(name__icontains="too") <co xml:id="CO7-2"/></programlisting>
<calloutlist>
<callout arearefs="CO7-1">
<para>Ca, c&#8217;est notre manager.</para>
</callout>
<callout arearefs="CO7-2">
<para>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".</para>
</callout>
</calloutlist>
<simpara>Pour un 'OR', on a deux options :</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Soit passer par deux querysets, typiuqment <literal>queryset1 | queryset2</literal></simpara>
</listitem>
<listitem>
<simpara>Soit passer par des <literal>Q objects</literal>, que l&#8217;on trouve dans le namespace <literal>django.db.models</literal>.</simpara>
</listitem>
</orderedlist>
<programlisting language="python" linenumbering="unnumbered">from django.db.models import Q
condition1 = Q(...)
condition2 = Q(...)
bidule.objects.filter(condition1 | condition2)</programlisting>
<simpara>L&#8217;opérateur inverse (<emphasis>NOT</emphasis>)</simpara>
<simpara>Idem que ci-dessus : soit on utilise la méthode <literal>exclude</literal> sur le queryset, soit l&#8217;opérateur <literal>~</literal> sur un Q object;</simpara>
<simpara>Ajouter les sujets suivants :</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Prefetch</simpara>
</listitem>
<listitem>
<simpara>select_related</simpara>
</listitem>
</orderedlist>
<section xml:id="_gestionnaire_de_models_managers_et_opérations">
<title>Gestionnaire de models (managers) et opérations</title>
<simpara>Chaque définition de modèle utilise un <literal>Manager</literal>, afin d&#8217;accéder à la base de données et traiter nos demandes.
Indirectement, une instance de modèle ne <emphasis role="strong">connait</emphasis> <emphasis role="strong">pas</emphasis> la base de données: c&#8217;est son gestionnaire qui a cette tâche.
Il existe deux exceptions à cette règle: les méthodes <literal>save()</literal> et <literal>update()</literal>.</simpara>
<itemizedlist>
<listitem>
<simpara>Instanciation: MyClass()</simpara>
</listitem>
<listitem>
<simpara>Récupération: MyClass.objects.get(pk=&#8230;&#8203;)</simpara>
</listitem>
<listitem>
<simpara>Sauvegarde : MyClass().save()</simpara>
</listitem>
<listitem>
<simpara>Création: MyClass.objects.create(&#8230;&#8203;)</simpara>
</listitem>
<listitem>
<simpara>Liste des enregistrements: MyClass.objects.all()</simpara>
</listitem>
</itemizedlist>
<simpara>Par défaut, le gestionnaire est accessible au travers de la propriété <literal>objects</literal>.
Cette propriété a une double utilité:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Elle est facile à surcharger - il nous suffit de définir une nouvelle classe héritant de ModelManager, puis de définir, au niveau de la classe, une nouvelle assignation à la propriété <literal>objects</literal></simpara>
</listitem>
<listitem>
<simpara>Il est tout aussi facile de définir d&#8217;autres propriétés présentant des filtres bien spécifiques.</simpara>
</listitem>
</orderedlist>
</section>
<section xml:id="_requêtes">
<title>Requêtes</title>
<simpara>DANGER: Les requêtes sont sensibles à la casse, <emphasis role="strong">même</emphasis> si le moteur de base de données ne l&#8217;est pas.
C&#8217;est notamment le cas pour Microsoft SQL Server; faire une recherche directement via les outils de Microsoft ne retournera pas
obligatoirement les mêmes résultats que les managers, qui seront beaucoup plus tatillons sur la qualité des recherches par
rapport aux filtres paramétrés en entrée.</simpara>
</section>
<section xml:id="_jointures">
<title>Jointures</title>
<simpara>Pour appliquer une jointure sur un modèle, nous pouvons passer par les méthodes <literal>select_related</literal> et <literal>prefetch_related</literal>.
Il faut cependant faire <emphasis role="strong">très</emphasis> attention au prefetch related, qui fonctionne en fait comme une grosse requête dans laquelle
nous trouvons un <literal>IN (&#8230;&#8203;)</literal>.
Càd que Django va récupérer tous les objets demandés initialement par le queryset, pour ensuite prendre toutes les clés primaires,
pour finalement faire une deuxième requête et récupérer les relations externes.</simpara>
<simpara>Au final, si votre premier queryset est relativement grand (nous parlons de 1000 à 2000 éléments, en fonction du moteur de base de données),
la seconde requête va planter et vous obtiendrez une exception de type <literal>django.db.utils.OperationalError: too many SQL variables</literal>.</simpara>
<simpara>Nous pourrions penser qu&#8217;utiliser un itérateur permettrait de combiner les deux, mais ce n&#8217;est pas le cas&#8230;&#8203;</simpara>
<simpara>Comme l&#8217;indique la documentation:</simpara>
<literallayout class="monospaced">Note that if you use iterator() to run the query, prefetch_related() calls will be ignored since these two optimizations do not make sense together.</literallayout>
<simpara>Ajouter un itérateur va en fait forcer le code à parcourir chaque élément de la liste, pour l&#8217;évaluer.
Il y aura donc (à nouveau) autant de requêtes qu&#8217;il y a d&#8217;éléments, ce que nous cherchons à éviter.</simpara>
<programlisting language="python" linenumbering="unnumbered">informations = (
&lt;MyObject&gt;.objects.filter(&lt;my_criteria&gt;)
.select_related(&lt;related_field&gt;)
.prefetch_related(&lt;related_field&gt;)
.iterator(chunk_size=1000)
)</programlisting>
</section>
</section>
<section xml:id="_aggregate_vs_annotate">
<title>Aggregate vs. Annotate</title>
<simpara><link xl:href="https://docs.djangoproject.com/en/3.1/topics/db/aggregation/">https://docs.djangoproject.com/en/3.1/topics/db/aggregation/</link></simpara>
</section>
</chapter>
<chapter xml:id="_migrations">
<title>Migrations</title>
<simpara>Dans cette section, nous allons voir comment fonctionnent les migrations.
Lors d&#8217;une première approche, elles peuvent sembler un peu magiques, puisqu&#8217;elles centralisent un ensemble de modifications pouvant être répétées sur un schéma de données, en tenant compte de ce qui a déjà été appliqué et en vérifiant quelles migrations devaient encore l&#8217;être pour mettre l&#8217;application à niveau.
Une analyse en profondeur montrera qu&#8217;elles ne sont pas plus complexes à suivre et à comprendre qu&#8217;un ensemble de fonctions de gestion appliquées à notre application.</simpara>
<note>
<simpara>La commande <literal>sqldump</literal>, qui nous présentera le schéma tel qu&#8217;il sera compris.</simpara>
</note>
<simpara>L&#8217;intégration des migrations a été réalisée dans la version 1.7 de Django.
Avant cela, il convenait de passer par une librairie tierce intitulée <link xl:href="https://south.readthedocs.io/en/latest">South</link>.</simpara>
<simpara>Prenons l&#8217;exemple de notre liste de souhaits; nous nous rendons (bêtement) compte que nous avons oublié d&#8217;ajouter un champ de <literal>description</literal> à une liste.
Historiquement, cette action nécessitait l&#8217;intervention d&#8217;un administrateur système ou d&#8217;une personne ayant accès au schéma de la base de données, à partir duquel ce-dit utilisateur pouvait jouer manuellement un script SQL. <indexterm>
<primary>sql</primary>
</indexterm>
Cet enchaînement d&#8217;étapes nécessitait une bonne coordination d&#8217;équipe, mais également une bonne confiance dans les scripts à exécuter.
Et souvenez-vous (cf. ref-à-insérer), que l&#8217;ensemble des actions doit être répétable et automatisable.</simpara>
<simpara>Bref, dans les années '80, il convenait de jouer ceci après s&#8217;être connecté au serveur de base de données:</simpara>
<programlisting language="sql" linenumbering="unnumbered">ALTER TABLE WishList ADD COLUMN Description nvarchar(MAX);</programlisting>
<simpara>Et là, nous nous rappelons qu&#8217;un utilisateur tourne sur Oracle et pas sur MySQL, et qu&#8217;il a donc besoin de son propre script d&#8217;exécution, parce que le type du nouveau champ n&#8217;est pas exactement le même entre les deux moteurs différents:</simpara>
<programlisting language="sql" linenumbering="unnumbered">-- Firebird
ALTER TABLE Category ALTER COLUMN Name type varchar(2000)
-- MSSQL
ALTER TABLE Category ALTER Column Name varchar(2000)
-- Oracle
ALTER TABLE Category MODIFY Name varchar2(2000)</programlisting>
<simpara>En bref, les problèmes suivants apparaissent très rapidement:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Aucune autonomie: il est nécessaire d&#8217;avoir les compétences d&#8217;une personne tierce pour avancer ou de disposer des droits administrateurs,</simpara>
</listitem>
<listitem>
<simpara>Aucune automatisation possible, à moins d&#8217;écrire un programme, qu&#8217;il faudra également maintenir et intégrer au niveau des tests</simpara>
</listitem>
<listitem>
<simpara>Nécessité de maintenir autant de scripts différents qu&#8217;il y a de moteurs de base de données supportés</simpara>
</listitem>
<listitem>
<simpara>Aucune possibilité de vérifier si le script a déjà été exécuté ou non, à moins, à nouveau, de maintenir un programme supplémentaire.</simpara>
</listitem>
</orderedlist>
<section xml:id="_fonctionement_général_2">
<title>Fonctionement général</title>
<simpara>Le moteur de migrations résout la plupart de ces soucis: le framework embarque ses propres applications, dont les migrations, qui gèrent elles-mêmes l&#8217;arbre de dépendances entre les modifications devant être appliquées.</simpara>
<simpara>Pour reprendre un des premiers exemples, nous avions créé un modèle contenant deux classes, qui correspondent chacun à une table dans un modèle relationnel:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)</programlisting>
<simpara>Nous avions ensuite modifié la clé de liaison, pour permettre d&#8217;associer plusieurs catégories à un même livre, et inversément:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category, on_delete=models.CASCADE)</programlisting>
<simpara>Chronologiquement, cela nous a donné une première migration consistant à créer le modèle initial, suivie d&#8217;une seconde migration après que nous ayons modifié le modèle pour autoriser des relations multiples.</simpara>
<simpara>migrations successives, à appliquer pour que la structure relationnelle corresponde aux attentes du modèle Django:</simpara>
<programlisting language="python" linenumbering="unnumbered"># library/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel( <co xml:id="CO8-1"/>
name="Category",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
],
),
migrations.CreateModel( <co xml:id="CO8-2"/>
name="Book",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"title",
models.CharField(max_length=255)),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="library.category",
),
),
],
),
]</programlisting>
<calloutlist>
<callout arearefs="CO8-1">
<para>La migration crée un nouveau modèle intitulé "Category", possédant un champ <literal>id</literal> (auto-défini, puisque nous n&#8217;avons rien fait), ainsi qu&#8217;un champ <literal>name</literal> de type texte et d&#8217;une longue maximale de 255 caractères.</para>
</callout>
<callout arearefs="CO8-2">
<para>Elle crée un deuxième modèle intitulé "Book", possédant trois champs: son identifiant auto-généré <literal>id</literal>, son titre <literal>title</literal> et sa relation vers une catégorie, au travers du champ <literal>category</literal>.</para>
</callout>
</calloutlist>
<simpara>Un outil comme <link xl:href="https://sqlitebrowser.org/">DB Browser For SQLite</link> nous donne la structure suivante:</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/db/migrations-0001-to-0002.png"/>
</imageobject>
<textobject><phrase>migrations 0001 to 0002</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>La représentation au niveau de la base de données est la suivante:</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/db/link-book-category-fk.drawio.png"/>
</imageobject>
<textobject><phrase>link book category fk.drawio</phrase></textobject>
</mediaobject>
</informalfigure>
<programlisting language="python" linenumbering="unnumbered">class Category(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category) <co xml:id="CO9-1"/></programlisting>
<calloutlist>
<callout arearefs="CO9-1">
<para>Vous noterez que l&#8217;attribut <literal>on_delete</literal> n&#8217;est plus nécessaire.</para>
</callout>
</calloutlist>
<simpara>Après cette modification, la migration résultante à appliquer correspondra à ceci.
En SQL, un champ de type <literal>ManyToMany</literal> ne peut qu&#8217;être représenté par une table intermédiaire.
C&#8217;est qu&#8217;applique la migration en supprimant le champ liant initialement un livre à une catégorie et en ajoutant une nouvelle table de liaison.</simpara>
<programlisting language="python" linenumbering="unnumbered"># library/migrations/0002_remove_book_category_book_category.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField( <co xml:id="CO10-1"/>
model_name='book',
name='category',
),
migrations.AddField( <co xml:id="CO10-2"/>
model_name='book',
name='category',
field=models.ManyToManyField(to='library.Category'),
),
]</programlisting>
<calloutlist>
<callout arearefs="CO10-1">
<para>La migration supprime l&#8217;ancienne clé étrangère&#8230;&#8203;</para>
</callout>
<callout arearefs="CO10-2">
<para>&#8230;&#8203; et ajoute une nouvelle table, permettant de lier nos catégories à nos livres.</para>
</callout>
</calloutlist>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/db/migrations-0002-many-to-many.png"/>
</imageobject>
<textobject><phrase>migrations 0002 many to many</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>Nous obtenons à présent la représentation suivante en base de données:</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/db/link-book-category-m2m.drawio.png"/>
</imageobject>
<textobject><phrase>link book category m2m.drawio</phrase></textobject>
</mediaobject>
</informalfigure>
</section>
<section xml:id="_graph_de_dépendances">
<title>Graph de dépendances</title>
<simpara>Lorsqu&#8217;une migration applique une modification au schéma d&#8217;une base de données, il est évident qu&#8217;elle ne peut pas être appliquée dans n&#8217;importe quel ordre ou à n&#8217;importe quel moment.</simpara>
<simpara>Dès la création d&#8217;un nouveau projet, avec une configuration par défaut et même sans avoir ajouté d&#8217;applications, Django proposera immédiatement d&#8217;appliquer les migrations des applications <emphasis role="strong">admin</emphasis>, <emphasis role="strong">auth</emphasis>, <emphasis role="strong">contenttypes</emphasis> et <emphasis role="strong">sessions</emphasis>, qui font partie du coeur du système, et qui se trouvent respectivement aux emplacements suivants:</simpara>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">admin</emphasis>: <literal>site-packages/django/contrib/admin/migrations</literal></simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">auth</emphasis>: <literal>site-packages/django/contrib/auth/migrations</literal></simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">contenttypes</emphasis>: <literal>site-packages/django/contrib/contenttypes/migrations</literal></simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">sessions</emphasis>: <literal>site-packages/django/contrib/sessions/migrations</literal></simpara>
</listitem>
</itemizedlist>
<simpara>Ceci est dû au fait que, toujours par défaut, ces applications sont reprises au niveau de la configuration d&#8217;un nouveau projet, dans le fichier <literal>settings.py</literal>:</simpara>
<programlisting language="python" linenumbering="unnumbered">[snip]
INSTALLED_APPS = [
'django.contrib.admin', <co xml:id="CO11-1"/>
'django.contrib.auth', <co xml:id="CO11-2"/>
'django.contrib.contenttypes', <co xml:id="CO11-3"/>
'django.contrib.sessions', <co xml:id="CO11-4"/>
'django.contrib.messages',
'django.contrib.staticfiles',
]
[snip]</programlisting>
<calloutlist>
<callout arearefs="CO11-1">
<para>Admin</para>
</callout>
<callout arearefs="CO11-2">
<para>Auth</para>
</callout>
<callout arearefs="CO11-3">
<para>Contenttypes</para>
</callout>
<callout arearefs="CO11-4">
<para>et Sessions.</para>
</callout>
</calloutlist>
<simpara>Dès que nous les appliquerons, nous recevrons les messages suivants:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, library, sessions, world
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK</programlisting>
<simpara>Cet ordre est défini au niveau de la propriété <literal>dependencies</literal>, que l&#8217;on retrouve au niveau de chaque description de migration,
En explorant les paquets qui se trouvent au niveau des répertoires et en analysant les dépendances décrites au niveau de chaque action de migration, on arrive au schéma suivant, qui est un graph dirigé acyclique:</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/db/migrations_auth_admin_contenttypes_sessions.png"/>
</imageobject>
<textobject><phrase>migrations auth admin contenttypes sessions</phrase></textobject>
</mediaobject>
</informalfigure>
</section>
<section xml:id="_sous_le_capot">
<title>Sous le capot</title>
<simpara>Une migration consiste à appliquer un ensemble de modifications (ou <emphasis role="strong">opérations</emphasis>), qui exercent un ensemble de transformations, pour que le schéma de base de données corresponde au modèle de l&#8217;application sous-jacente.</simpara>
<simpara>Les migrations (comprendre les "<emphasis>migrations du schéma de base de données</emphasis>") sont intimement liées à la représentation d&#8217;un contexte fonctionnel: l&#8217;ajout d&#8217;une nouvelle information, d&#8217;un nouveau champ ou d&#8217;une nouvelle fonction peut s&#8217;accompagner de tables de données à mettre à jour ou de champs à étendre.
Il est primordial que la structure de la base de données corresponde à ce à quoi l&#8217;application s&#8217;attend, sans quoi la probabilité que l&#8217;utilisateur tombe sur une erreur de type <literal>django.db.utils.OperationalError</literal> est (très) grande.
Typiquement, après avoir ajouté un nouveau champ <literal>summary</literal> à chacun de nos livres, et sans avoir appliqué de migrations, nous tombons sur ceci:</simpara>
<programlisting language="shell" linenumbering="unnumbered">&gt;&gt;&gt; from library.models import Book
&gt;&gt;&gt; Book.objects.all()
Traceback (most recent call last):
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
File "~/Sources/.venvs/gwlib/lib/python3.9/site-packages/django/db/backends/sqlite3/base.py", line 416, in execute
return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such column: library_book.summary</programlisting>
<simpara>Pour éviter ce type d&#8217;erreurs, il est impératif que les nouvelles migrations soient appliquées <emphasis role="strong">avant</emphasis> que le code ne soit déployé; l&#8217;idéal étant que ces deux opérations soient réalisées de manière atomique, avec un <emphasis>rollback</emphasis> si une anomalie était détectée.</simpara>
<simpara>En allant</simpara>
<simpara>Pour éviter ce type d&#8217;erreurs, plusieurs stratégies peuvent être appliquées:</simpara>
<simpara>intégrer ici un point sur les updates db - voir designing data-intensive applications.</simpara>
<simpara>Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code, et doivent faire partie du dépôt central de sources.
Le développeur s&#8217;occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, <emphasis>squashées</emphasis>, &#8230;&#8203; et feront partie intégrante du processus de mise à jour de l&#8217;application.</simpara>
<simpara>A noter que les migrations n&#8217;appliqueront de modifications que si le schéma est impacté.
Ajouter une propriété <literal>related_name</literal> sur une ForeignKey n&#8217;engendrera aucune nouvelle action de migration, puisque ce type d&#8217;action ne s&#8217;applique que sur l&#8217;ORM, et pas directement sur la base de données: au niveau des tables, rien ne change.
Seul le code et le modèle sont impactés.</simpara>
<simpara>Une migration est donc une classe Python, présentant <emphasis>a minima</emphasis> deux propriétés:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara><literal>dependencies</literal>, qui décrit les opérations précédentes devant obligatoirement avoir été appliquées</simpara>
</listitem>
<listitem>
<simpara><literal>operations</literal>, qui consiste à décrire précisément ce qui doit être exécuté.</simpara>
</listitem>
</orderedlist>
<simpara>Pour reprendre notre exemple d&#8217;ajout d&#8217;un champ <literal>description</literal> sur le modèle <literal>WishList</literal>, la migration ressemblera à ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered">from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('gwift', '0004_name_value'),
]
operations = [
migrations.AddField(
model_name='wishlist',
name='description',
field=models.TextField(default="", null=True)
preserve_default=False,
),
]</programlisting>
</section>
<section xml:id="_liste_des_migrations">
<title>Liste des migrations</title>
<simpara>L&#8217;option <literal>showmigrations</literal> de <literal>manage.py</literal> permet de lister toutes les migrations du projet, et d&#8217;identifier celles qui n&#8217;auraient pas encore été appliquées:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ python manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
[X] 0010_alter_group_name_max_length
[X] 0011_update_proxy_permissions
[X] 0012_alter_user_first_name_max_length
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
library
[X] 0001_initial
[X] 0002_remove_book_category_book_category
[ ] 0003_book_summary
sessions
[X] 0001_initial</programlisting>
</section>
<section xml:id="_squash">
<title>Squash</title>
<simpara>Finalement, lorsque vous développez sur votre propre branche (cf. <xref linkend="git"/>), vous serez peut-être tentés de créer plusieurs migrations en fonction de l&#8217;évolution de ce que vous mettez en place.
Dans ce cas précis, il peut être intéressant d&#8217;utiliser la méthode <literal>squashmigrations</literal>, qui permet <emphasis>d&#8217;aplatir</emphasis> plusieurs fichiers en un seul.</simpara>
<simpara>Nous partons dans deux migrations suivantes:</simpara>
<programlisting language="python" linenumbering="unnumbered"># library/migrations/0002_remove_book_category.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.Category'),
),
]</programlisting>
<programlisting language="python" linenumbering="unnumbered"># library/migrations/0003_book_summary.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0002_remove_book_category_book_category'),
]
operations = [
migrations.AddField(
model_name='book',
name='summary',
field=models.TextField(blank=True),
),
]</programlisting>
<simpara>La commande <literal>python manage.py squashmigrations library 0002 0003</literal> appliquera une fusion entre les migrations numérotées <literal>0002</literal> et <literal>0003</literal>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ python manage.py squashmigrations library 0002 0003
Will squash the following migrations:
- 0002_remove_book_category_book_category
- 0003_book_summary
Do you wish to proceed? [yN] y
Optimizing...
No optimizations possible.
Created new squashed migration /home/fred/Sources/gwlib/library/migrations/0002_remove_book_category_book_category_squashed_0003_book_summary.py
You should commit this migration but leave the old ones in place;
the new migration will be used for new installs. Once you are sure
all instances of the codebase have applied the migrations you squashed,
you can delete them.</programlisting>
<warning>
<simpara>Dans le cas où vous développez proprement (bis), il est sauf de purement et simplement supprimer les anciens fichiers; dans le cas où il pourrait exister au moins une instance ayant appliqué ces migrations, les anciens <emphasis role="strong">ne peuvent surtout pas être modifiés</emphasis>.</simpara>
</warning>
<simpara>Nous avons à présent un nouveau fichier intitulé <literal>0002_remove_book_category_book_category_squashed_0003_book_summary</literal>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ cat library/migrations/0002_remove_book_category_book_category_squashed_0003_book_summary.py
# Generated by Django 4.0.3 on 2022-03-15 18:01
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('library', '0002_remove_book_category_book_category'), ('library', '0003_book_summary')]
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='category',
),
migrations.AddField(
model_name='book',
name='category',
field=models.ManyToManyField(to='library.category'),
),
migrations.AddField(
model_name='book',
name='summary',
field=models.TextField(blank=True),
),
]</programlisting>
</section>
<section xml:id="_réinitialisation_dune_ou_plusieurs_migrations">
<title>Réinitialisation d&#8217;une ou plusieurs migrations</title>
<simpara><link xl:href="https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html">reset migrations</link>.</simpara>
<blockquote>
<literallayout class="monospaced"> En gros, soit on supprime toutes les migrations (en conservant le fichier __init__.py), soit on réinitialise proprement les migrations avec un --fake-initial (sous réserve que toutes les personnes qui utilisent déjà le projet s'y conforment... Ce qui n'est pas gagné.
Pour repartir de notre exemple ci-dessus, nous avions un modèle reprenant quelques classes, saupoudrées de propriétés décrivant nos différents champs. Pour être prise en compte par le moteur de base de données, chaque modification doit être</literallayout>
</blockquote>
</section>
</chapter>
<chapter xml:id="_shell">
<title>Shell</title>
</chapter>
<chapter xml:id="_administration">
<title>Administration</title>
<simpara>Woké. On va commencer par la <emphasis role="strong">partie à ne <emphasis>surtout</emphasis> (<emphasis>surtout</emphasis> !!) pas faire en premier dans un projet Django</emphasis>.
Mais on va la faire quand même: la raison principale est que cette partie est tellement puissante et performante, qu&#8217;elle pourrait laisser penser qu&#8217;il est possible de réaliser une application complète rien qu&#8217;en configurant l&#8217;administration.
Mais c&#8217;est faux.</simpara>
<simpara>L&#8217;administration est une sorte de tour de contrôle évoluée, un <emphasis>back office</emphasis> sans transpirer; elle se base sur le modèle de données programmé et construit dynamiquement les formulaires qui lui est associé.
Elle joue avec les clés primaires, étrangères, les champs et types de champs par <link xl:href="https://fr.wikipedia.org/wiki/Introspection">introspection</link>, et présente tout ce qu&#8217;il faut pour avoir du <link xl:href="https://fr.wikipedia.org/wiki/CRUD">CRUD</link>, c&#8217;est-à-dire tout ce qu&#8217;il faut pour ajouter, lister, modifier ou supprimer des informations.</simpara>
<simpara>Son problème est qu&#8217;elle présente une courbe d&#8217;apprentissage asymptotique.
Il est <emphasis role="strong">très</emphasis> facile d&#8217;arriver rapidement à un bon résultat, au travers d&#8217;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.</simpara>
<simpara>Cette fonctionnalité doit rester dans les mains d&#8217;administrateurs ou de gestionnaires, et dans leurs mains à eux uniquement: il n&#8217;est pas question de donner des droits aux utilisateurs finaux (même si c&#8217;est extrêment tentant durant les premiers tours de roues).
Indépendamment de la manière dont vous allez l&#8217;utiliser et la configurer, vous finirez par devoir développer une "vraie" application, destinée aux utilisateurs classiques, et répondant à leurs besoins uniquement.</simpara>
<simpara>Une bonne idée consiste à développer l&#8217;administration dans un premier temps, en <emphasis role="strong">gardant en tête qu&#8217;il sera nécessaire de développer des concepts spécifiques</emphasis>.
Dans cet objectif, l&#8217;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.</simpara>
<simpara>C&#8217;est aussi un excellent outil de prototypage et de preuve de concept.</simpara>
<simpara>Elle se base sur plusieurs couches que l&#8217;on a déjà (ou on va bientôt) aborder (suivant le sens de lecture que vous préférez):</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Le modèle de données</simpara>
</listitem>
<listitem>
<simpara>Les validateurs</simpara>
</listitem>
<listitem>
<simpara>Les formulaires</simpara>
</listitem>
<listitem>
<simpara>Les widgets</simpara>
</listitem>
</orderedlist>
<section xml:id="_le_modèle_de_données">
<title>Le modèle de données</title>
<simpara>Comme expliqué ci-dessus, le modèle de données est constité d&#8217;un ensemble de champs typés et de relations.
L&#8217;administration permet de décrire les données qui peuvent être modifiées, en y associant un ensemble (basique) de permissions.</simpara>
<simpara>Si vous vous rappelez de l&#8217;application que nous avions créée dans la première partie, les URLs reprenaient déjà la partie suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered">from django.contrib import admin
from django.urls import path
from gwift.views import wish_details
urlpatterns = [
path('admin/', admin.site.urls), <co xml:id="CO12-1"/>
[...]
]</programlisting>
<calloutlist>
<callout arearefs="CO12-1">
<para>Cette URL signifie que la partie <literal>admin</literal> est déjà active et accessible à l&#8217;URL <literal>&lt;mon_site&gt;/admin</literal></para>
</callout>
</calloutlist>
<simpara>C&#8217;est le seul prérequis pour cette partie.</simpara>
<simpara>Chaque application nouvellement créée contient par défaut un fichier <literal>admin.py</literal>, dans lequel il est possible de déclarer quel ensemble de données sera accessible/éditable.
Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits:</simpara>
<programlisting language="python" linenumbering="unnumbered"># gwift/wish/models.py
from django.db import models
class WishList(models.Model):
name = models.CharField(max_length=255)
class Item(models.Model):
name = models.CharField(max_length=255)
wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE)</programlisting>
<simpara>Nous pouvons facilement arriver au résultat suivant, en ajoutant quelques lignes de configuration dans ce fichier <literal>admin.py</literal>:</simpara>
<programlisting language="python" linenumbering="unnumbered">from django.contrib import admin
from .models import Item, WishList <co xml:id="CO13-1"/>
admin.site.register(Item) <co xml:id="CO13-2"/>
admin.site.register(WishList)</programlisting>
<calloutlist>
<callout arearefs="CO13-1">
<para>Nous importons les modèles que nous souhaitons gérer dans l&#8217;admin</para>
</callout>
<callout arearefs="CO13-2">
<para>Et nous les déclarons comme gérables. Cette dernière ligne implique aussi qu&#8217;un modèle pourrait ne pas être disponible du tout, ce qui n&#8217;activera simplement aucune opération de lecture ou modification.</para>
</callout>
</calloutlist>
<simpara>Il nous reste une seule étape à réaliser: créer un nouvel utilisateur.
Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d&#8217;un <emphasis>super-utilisateur</emphasis>, que nous pouvons créer grâce à la commande <literal>python manage.py createsuperuser</literal>.</simpara>
<programlisting language="bash" linenumbering="unnumbered">λ python manage.py createsuperuser
Username (leave blank to use 'fred'): fred
Email address: fred@root.org
Password: ******
Password (again): ******
Superuser created successfully.</programlisting>
<figure>
<title>Connexion au site d&#8217;administration</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/django-site-admin.png" align="center"/>
</imageobject>
<textobject><phrase>django site admin</phrase></textobject>
</mediaobject>
</figure>
<figure>
<title>Administration</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/django-site-admin-after-connection.png" align="center"/>
</imageobject>
<textobject><phrase>django site admin after connection</phrase></textobject>
</mediaobject>
</figure>
</section>
<section xml:id="_quelques_conseils_de_base">
<title>Quelques conseils de base</title>
<orderedlist numeration="arabic">
<listitem>
<simpara>Surchargez la méthode <literal><emphasis>str</emphasis>(self)</literal> pour chaque classe que vous aurez définie dans le modèle. Cela permettra de construire une représentation textuelle pour chaque instance de votre classe. Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l&#8217;on manipule. En plus, cette méthode est également appelée lorsque l&#8217;administration historisera une action (et comme cette étape sera inaltérable, autant qu&#8217;elle soit fixée dans le début).</simpara>
</listitem>
<listitem>
<simpara>La méthode <literal>get_absolute_url(self)</literal> retourne l&#8217;URL à laquelle on peut accéder pour obtenir les détails d&#8217;une instance. Par exemple:</simpara>
</listitem>
</orderedlist>
<programlisting language="python" linenumbering="unnumbered">def get_absolute_url(self):
return reverse('myapp.views.details', args=[self.id])</programlisting>
<orderedlist numeration="arabic">
<listitem>
<simpara>Les attributs <literal>Meta</literal>:</simpara>
</listitem>
</orderedlist>
<programlisting language="python" linenumbering="unnumbered">class Meta:
ordering = ['-field1', 'field2']
verbose_name = 'my class in singular'
verbose_name_plural = 'my class when is in a list!'</programlisting>
<orderedlist numeration="arabic">
<listitem>
<simpara>Le titre:</simpara>
<itemizedlist>
<listitem>
<simpara>Soit en modifiant le template de l&#8217;administration</simpara>
</listitem>
<listitem>
<simpara>Soit en ajoutant l&#8217;assignation suivante dans le fichier <literal>urls.py</literal>: <literal>admin.site.site_header = "SuperBook Secret Area</literal>.</simpara>
</listitem>
</itemizedlist>
</listitem>
<listitem>
<simpara>Prefetch</simpara>
</listitem>
</orderedlist>
<simpara><link xl:href="https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64">https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64</link></simpara>
<simpara><link xl:href="https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614">https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614</link></simpara>
<simpara>En gros, le problème de l&#8217;admin est que si on fait des requêtes imbriquées, on va flinguer l&#8217;application et le chargement de la page.
La solution consiste à utiliser la propriété <literal>list_select_related</literal> de la classe d&#8217;Admin, afin d&#8217;appliquer une jointure par défaut et
et gagner en performances.</simpara>
</section>
<section xml:id="_admin_modeladmin">
<title>admin.ModelAdmin</title>
<simpara>La classe <literal>admin.ModelAdmin</literal> que l&#8217;on retrouvera principalement dans le fichier <literal>admin.py</literal> de chaque application contiendra la définition de ce que l&#8217;on souhaite faire avec nos données dans l&#8217;administration. Cette classe (et sa partie Meta)</simpara>
</section>
<section xml:id="_laffichage">
<title>L&#8217;affichage</title>
<simpara>Comme l&#8217;interface d&#8217;administration fonctionne (en trèèèès) gros comme un CRUD auto-généré, on trouve par défaut la possibilité de :</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Créer de nouveaux éléments</simpara>
</listitem>
<listitem>
<simpara>Lister les éléments existants</simpara>
</listitem>
<listitem>
<simpara>Modifier des éléments existants</simpara>
</listitem>
<listitem>
<simpara>Supprimer un élément en particulier.</simpara>
</listitem>
</orderedlist>
<simpara>Les affichages sont donc de deux types: en liste et par élément.</simpara>
<simpara>Pour les affichages en liste, le plus simple consiste à jouer sur la propriété <literal>list_display</literal>.</simpara>
<simpara>Par défaut, la première colonne va accueillir le lien vers le formulaire d&#8217;édition.
On peut donc modifier ceci, voire créer de nouveaux liens vers d&#8217;autres éléments en construisant des URLs dynamiquement.</simpara>
<simpara>(Insérer ici l&#8217;exemple de Medplan pour les liens vers les postgradués :-))</simpara>
<simpara>Voir aussi comment personnaliser le fil d&#8217;Ariane ?</simpara>
</section>
<section xml:id="_les_filtres">
<title>Les filtres</title>
<orderedlist numeration="arabic">
<listitem>
<simpara>list_filter</simpara>
</listitem>
<listitem>
<simpara>filter_horizontal</simpara>
</listitem>
<listitem>
<simpara>filter_vertical</simpara>
</listitem>
<listitem>
<simpara>date_hierarchy</simpara>
</listitem>
</orderedlist>
</section>
<section xml:id="_les_permissions">
<title>Les permissions</title>
<simpara>On l&#8217;a dit plus haut, il vaut mieux éviter de proposer un accès à l&#8217;administration à vos utilisateurs.
Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression.</simpara>
<simpara>Cela se joue au niveau du <literal>ModelAdmin</literal>, en implémentant les méthodes suivantes:</simpara>
<programlisting language="python" linenumbering="unnumbered">def has_add_permission(self, request):
return True
def has_delete_permission(self, request):
return True
def has_change_permission(self, request):
return True</programlisting>
<simpara>On peut accéder aux informations de l&#8217;utilisateur actuellement connecté au travers de l&#8217;objet <literal>request.user</literal>.</simpara>
<orderedlist numeration="loweralpha">
<listitem>
<simpara>NOTE: ajouter un ou deux screenshots :-)</simpara>
</listitem>
</orderedlist>
</section>
<section xml:id="_les_relations">
<title>Les relations</title>
<section xml:id="_les_relations_1_n">
<title>Les relations 1-n</title>
<simpara>Les relations 1-n sont implémentées au travers de formsets (que l&#8217;on a normalement déjà décrits plus haut). L&#8217;administration permet de les définir d&#8217;une manière extrêmement simple, grâce à quelques propriétés.</simpara>
<simpara>L&#8217;implémentation consiste tout d&#8217;abord à définir le comportement du type d&#8217;objet référencé (la relation -N), puis à inclure cette définition au niveau du type d&#8217;objet référençant (la relation 1-).</simpara>
<programlisting language="python" linenumbering="unnumbered">class WishInline(TabularInline):
model = Wish
class Wishlist(admin.ModelAdmin):
...
inlines = [WishInline]
...</programlisting>
<simpara>Et voilà : l&#8217;administration d&#8217;une liste de souhaits (<emphasis>Wishlist</emphasis>) pourra directement gérer des relations multiples vers des souhaits.</simpara>
</section>
<section xml:id="_les_auto_suggestions_et_auto_complétions">
<title>Les auto-suggestions et auto-complétions</title>
<simpara>Parler de l&#8217;intégration de select2.</simpara>
</section>
</section>
<section xml:id="_la_présentation">
<title>La présentation</title>
<simpara>Parler ici des <literal>fieldsets</literal> et montrer comment on peut regrouper des champs dans des groupes, ajouter un peu de javascript, &#8230;&#8203;</simpara>
</section>
<section xml:id="_les_actions_sur_des_sélections">
<title>Les actions sur des sélections</title>
<simpara>Les actions permettent de partir d&#8217;une liste d&#8217;éléments, et autorisent un utilisateur à appliquer une action sur une sélection d&#8217;éléments. Par défaut, il existe déjà une action de <emphasis role="strong">suppression</emphasis>.</simpara>
<simpara>Les paramètres d&#8217;entrée sont :</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>L&#8217;instance de classe</simpara>
</listitem>
<listitem>
<simpara>La requête entrante</simpara>
</listitem>
<listitem>
<simpara>Le queryset correspondant à la sélection.</simpara>
</listitem>
</orderedlist>
<programlisting language="python" linenumbering="unnumbered">def double_quantity(self, request, queryset):
for obj in queryset.all():
obj.field += 1
obj.save()
double_quantity.short_description = "Doubler la quantité des souhaits."</programlisting>
<simpara>Et pour informer l&#8217;utilisateur de ce qui a été réalisé, on peut aussi lui passer un petit message:</simpara>
<programlisting language="python" linenumbering="unnumbered">if rows_updated = 0:
self.message_user(request, "Aucun élément n'a été impacté.")
else:
self.message_user(request, "{} élément(s) mis à jour".format(rows_updated))</programlisting>
</section>
<section xml:id="_la_documentation">
<title>La documentation</title>
<simpara>Nous l&#8217;avons dit plus haut, l&#8217;administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données.
Pour cela, il suffit de suivre les bonnes pratiques, puis <link xl:href="https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/">d&#8217;activer la documentation à partir des URLs</link>:</simpara>
<programlisting language="python" linenumbering="unnumbered"></programlisting>
</section>
</chapter>
<chapter xml:id="_forms">
<title>Forms</title>
<blockquote>
<simpara>Le form, il s&#8217;assure que l&#8217;utilisateur n&#8217;a pas encodé de conneries et que l&#8217;ensemble reste cohérent.
Il (le form) n&#8217;a pas à savoir que tu as implémenté des closure tables dans un graph dirigé acyclique.</simpara>
</blockquote>
<simpara>Ou comment valider proprement des données entrantes.</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/xkcd-327.png"/>
</imageobject>
<textobject><phrase>xkcd 327</phrase></textobject>
</mediaobject>
</informalfigure>
<simpara>Quand on parle de <literal>forms</literal>, on ne parle pas uniquement de formulaires Web. On pourrait considérer qu&#8217;il s&#8217;agit de leur objectif principal, mais on peut également voir un peu plus loin: on peut en fait voir les <literal>forms</literal> comme le point d&#8217;entrée pour chaque donnée arrivant dans notre application: il s&#8217;agit en quelque sorte d&#8217;un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle.</simpara>
<simpara>L&#8217;exemple le plus simple est un fichier <literal>.csv</literal>: 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&#8217;introduisant dans une instance du modèle.</simpara>
<simpara>Mauvaise idée. On peut proposer trois versions d&#8217;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 <link xl:href="https://docs.python.org/3/library/csv.html#csv.DictReader">DictReader</link>), et la version + à base de form.</simpara>
<simpara>Les données fournies par un utilisateur <emphasis role="strong">doivent</emphasis> <emphasis role="strong">toujours</emphasis> être validées avant introduction dans la base de données. Notre base de données étant accessible ici par l&#8217;ORM, la solution consiste à introduire une couche supplémentaire de validation.</simpara>
<simpara>Le flux à suivre est le suivant:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Création d&#8217;une instance grâce à un dictionnaire</simpara>
</listitem>
<listitem>
<simpara>Validation des données et des informations reçues</simpara>
</listitem>
<listitem>
<simpara>Traitement, si la validation a réussi.</simpara>
</listitem>
</orderedlist>
<simpara>Ils jouent également deux rôles importants:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Valider des données, en plus de celles déjà définies au niveau du modèle</simpara>
</listitem>
<listitem>
<simpara>Contrôler le rendu à appliquer aux champs.</simpara>
</listitem>
</orderedlist>
<simpara>Ils agissent come une glue entre l&#8217;utilisateur et la modélisation de vos structures de données.</simpara>
<section xml:id="_flux_de_validation">
<title>Flux de validation</title>
<simpara>| .Validation
| .is_valid
| .clean_fields
↓ .clean_fields_machin</simpara>
<note>
<simpara>A compléter ;-)</simpara>
</note>
</section>
<section xml:id="_dépendance_avec_le_modèle">
<title>Dépendance avec le modèle</title>
<simpara>Un <emphasis role="strong">form</emphasis> peut dépendre d&#8217;une autre classe Django. Pour cela, il suffit de fixer l&#8217;attribut <literal>model</literal> au niveau de la <literal>class Meta</literal> dans la définition.</simpara>
<programlisting language="python" linenumbering="unnumbered">from django import forms
from wish.models import Wishlist
class WishlistCreateForm(forms.ModelForm):
class Meta:
model = Wishlist
fields = ('name', 'description')</programlisting>
<simpara>De cette manière, notre form dépendra automatiquement des champs déjà déclarés dans la classe <literal>Wishlist</literal>. Cela suit le principe de <literal>DRY &lt;don&#8217;t repeat yourself&gt;`_, et évite qu&#8217;une modification ne pourrisse le code: en testant les deux champs présent dans l&#8217;attribut `fields</literal>, nous pourrons nous assurer de faire évoluer le formulaire en fonction du modèle sur lequel il se base.</simpara>
</section>
<section xml:id="_rendu_et_affichage">
<title>Rendu et affichage</title>
<simpara>Le formulaire permet également de contrôler le rendu qui sera appliqué lors de la génération de la page. Si les champs dépendent du modèle sur lequel se base le formulaire, ces widgets doivent être initialisés dans l&#8217;attribut <literal>Meta</literal>. Sinon, ils peuvent l&#8217;être directement au niveau du champ.</simpara>
<programlisting language="python" linenumbering="unnumbered">from datetime import date
from django import forms
from .models import Accident
class AccidentForm(forms.ModelForm):
class Meta:
model = Accident
fields = ('gymnast', 'educative', 'date', 'information')
widgets = {
'date' : forms.TextInput(
attrs={
'class' : 'form-control',
'data-provide' : 'datepicker',
'data-date-format' : 'dd/mm/yyyy',
'placeholder' : date.today().strftime("%d/%m/%Y")
}),
'information' : forms.Textarea(
attrs={
'class' : 'form-control',
'placeholder' : 'Context (why, where, ...)'
})
}</programlisting>
</section>
<section xml:id="_squelette_par_défaut">
<title>Squelette par défaut</title>
<simpara>On a d&#8217;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 <literal>widget-tweaks</literal>.</simpara>
</section>
<section xml:id="_crispy_forms">
<title>Crispy-forms</title>
<simpara>Comme on l&#8217;a vu à l&#8217;instant, les forms, en Django, c&#8217;est le bien. Cela permet de valider des données reçues en entrée et d&#8217;afficher (très) facilement des formulaires à compléter par l&#8217;utilisateur.</simpara>
<simpara>Par contre, c&#8217;est lourd. Dès qu&#8217;on souhaite peaufiner un peu l&#8217;affichage, contrôler parfaitement ce que l&#8217;utilisateur doit remplir, modifier les types de contrôleurs, les placer au pixel près, &#8230;&#8203; Tout ça demande énormément de temps. Et c&#8217;est là qu&#8217;intervient <link xl:href="http://django-crispy-forms.readthedocs.io/en/latest/">Django-Crispy-Forms</link>. Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le <emphasis role="strong">layout</emphasis> et la présentation.</simpara>
<simpara>(c/c depuis le lien ci-dessous)</simpara>
<simpara>Pour chaque champ, crispy-forms va :</simpara>
<itemizedlist>
<listitem>
<simpara>utiliser le <literal>verbose_name</literal> comme label.</simpara>
</listitem>
<listitem>
<simpara>vérifier les paramètres <literal>blank</literal> et <literal>null</literal> pour savoir si le champ est obligatoire.</simpara>
</listitem>
<listitem>
<simpara>utiliser le type de champ pour définir le type de la balise <literal>&lt;input&gt;</literal>.</simpara>
</listitem>
<listitem>
<simpara>récupérer les valeurs du paramètre <literal>choices</literal> (si présent) pour la balise <literal>&lt;select&gt;</literal>.</simpara>
</listitem>
</itemizedlist>
<simpara><link xl:href="http://dotmobo.github.io/django-crispy-forms.html">http://dotmobo.github.io/django-crispy-forms.html</link></simpara>
</section>
<section xml:id="_en_conclusion">
<title>En conclusion</title>
<orderedlist numeration="arabic">
<listitem>
<simpara>Toute donnée entrée par l&#8217;utilisateur <emphasis role="strong">doit</emphasis> passer par une instance de <literal>form</literal>.</simpara>
</listitem>
<listitem>
<simpara>euh ?</simpara>
</listitem>
</orderedlist>
</section>
</chapter>
<chapter xml:id="_authentification">
<title>Authentification</title>
<simpara>Comme on l&#8217;a vu dans la partie sur le modèle, nous souhaitons que le créateur d&#8217;une liste puisse retrouver facilement les éléments qu&#8217;il aura créé. Ce dont nous n&#8217;avons pas parlé cependant, c&#8217;est la manière dont l&#8217;utilisateur va pouvoir créer son compte et s&#8217;authentifier. La <link xl:href="https://docs.djangoproject.com/en/stable/topics/auth/">documentation</link> est très complète, nous allons essayer de la simplifier au maximum. Accrochez-vous, le sujet peut être complexe.</simpara>
<section xml:id="_mécanisme_dauthentification">
<title>Mécanisme d&#8217;authentification</title>
<simpara>On peut schématiser le flux d&#8217;authentification de la manière suivante :</simpara>
<simpara>En gros:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>La personne accède à une URL qui est protégée (voir les décorateurs @login_required et le mixin LoginRequiredMixin)</simpara>
</listitem>
<listitem>
<simpara>Le framework détecte qu&#8217;il est nécessaire pour la personne de se connecter (grâce à un paramètre type LOGIN_URL)</simpara>
</listitem>
<listitem>
<simpara>Le framework présente une page de connexion ou un mécanisme d&#8217;accès pour la personne (template à définir)</simpara>
</listitem>
<listitem>
<simpara>Le framework récupère les informations du formulaire, et les transmets aux différents backends d&#8217;authentification, dans l&#8217;ordre</simpara>
</listitem>
<listitem>
<simpara>Chaque backend va appliquer la méthode <literal>authenticate</literal> en cascade, jusqu&#8217;à ce qu&#8217;un backend réponde True ou qu&#8217;aucun ne réponde</simpara>
</listitem>
<listitem>
<simpara>La réponse de la méthode authenticate doit être une instance d&#8217;un utilisateur, tel que définit parmi les paramètres généraux de l&#8217;application.</simpara>
</listitem>
</orderedlist>
<simpara>En résumé (bis):</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Une personne souhaite se connecter;</simpara>
</listitem>
<listitem>
<simpara>Les backends d&#8217;authentification s&#8217;enchaîne jusqu&#8217;à trouver une bonne correspondance. Si aucune correspondance n&#8217;est trouvée, on envoie la personne sur les roses.</simpara>
</listitem>
<listitem>
<simpara>Si OK, on retourne une instance de type current_user, qui pourra être utilisée de manière uniforme dans l&#8217;application.</simpara>
</listitem>
</orderedlist>
<simpara>Ci-dessous, on définit deux backends différents pour mieux comprendre les différentes possibilités:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Une authentification par jeton</simpara>
</listitem>
<listitem>
<simpara>Une authentification LDAP</simpara>
</listitem>
</orderedlist>
<programlisting language="python" linenumbering="unnumbered">from datetime import datetime
from django.contrib.auth import backends, get_user_model
from django.db.models import Q
from accounts.models import Token <co xml:id="CO14-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</programlisting>
<calloutlist>
<callout arearefs="CO14-1">
<para>Sous-entend qu&#8217;on a bien une classe qui permet d&#8217;accéder à ces jetons ;-)</para>
</callout>
</calloutlist>
<programlisting language="python" linenumbering="unnumbered">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)</programlisting>
<simpara>On peut résumer le mécanisme d&#8217;authentification de la manière suivante:</simpara>
<itemizedlist>
<listitem>
<simpara>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 <link xl:href="https://docs.djangoproject.com/en/stable/topics/auth/customizing/">ici</link>.</simpara>
</listitem>
<listitem>
<simpara>Si vous souhaitez modifier la manière dont l&#8217;utilisateur se connecte, alors vous devrez modifier le <emphasis role="strong">backend</emphasis>.</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_modification_du_modèle">
<title>Modification du modèle</title>
<simpara>Dans un premier temps, Django a besoin de manipuler <link xl:href="https://docs.djangoproject.com/en/1.9/ref/contrib/auth/#user-model">des instances de type <literal>django.contrib.auth.User</literal></link>. Cette classe implémente les champs suivants:</simpara>
<itemizedlist>
<listitem>
<simpara><literal>username</literal></simpara>
</listitem>
<listitem>
<simpara><literal>first_name</literal></simpara>
</listitem>
<listitem>
<simpara><literal>last_name</literal></simpara>
</listitem>
<listitem>
<simpara><literal>email</literal></simpara>
</listitem>
<listitem>
<simpara><literal>password</literal></simpara>
</listitem>
<listitem>
<simpara><literal>date_joined</literal>.</simpara>
</listitem>
</itemizedlist>
<simpara>D&#8217;autres champs, comme les groupes auxquels l&#8217;utilisateur est associé, ses permissions, savoir s&#8217;il est un super-utilisateur, &#8230;&#8203; 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&#8217;implémenter nos propres classes, puisqu&#8217;elles existent déjà :-)</simpara>
<simpara>Si vous souhaitez ajouter un champ, il existe trois manières de faire.</simpara>
</section>
<section xml:id="_extension_du_modèle_existant">
<title>Extension du modèle existant</title>
<simpara>Le plus simple consiste à créer une nouvelle classe, et à faire un lien de type <literal>OneToOne</literal> vers la classe <literal>django.contrib.auth.User</literal>. De cette manière, on ne modifie rien à la manière dont Django authentife ses utlisateurs: tout ce qu&#8217;on fait, c&#8217;est un lien vers une table nouvellement créée, comme on l&#8217;a déjà vu au point [&#8230;&#8203;voir l&#8217;héritage de modèle]. L&#8217;avantage de cette méthode, c&#8217;est qu&#8217;elle est extrêmement flexible, et qu&#8217;on garde les mécanismes Django standard. Le désavantage, c&#8217;est que pour avoir toutes les informations de notre utilisateur, on sera obligé d&#8217;effectuer une jointure sur le base de données, ce qui pourrait avoir des conséquences sur les performances.</simpara>
</section>
<section xml:id="_substitution">
<title>Substitution</title>
<simpara>Avant de commencer, sachez que cette étape doit être effectuée <emphasis role="strong">avant la première migration</emphasis>. Le plus simple sera de définir une nouvelle classe héritant de <literal>django.contrib.auth.User</literal> 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.</simpara>
<programlisting language="python" linenumbering="unnumbered">AUTH_USER_MODEL = 'myapp.MyUser'</programlisting>
<simpara>Notez bien qu&#8217;il ne faut pas spécifier le package <literal>.models</literal> dans cette injection de dépendances: le schéma à indiquer est bien <literal>&lt;nom de l&#8217;application&gt;.&lt;nom de la classe&gt;</literal>.</simpara>
<section xml:id="_backend">
<title>Backend</title>
</section>
<section xml:id="_templates">
<title>Templates</title>
<simpara>Ce qui n&#8217;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 &lt;<link xl:href="https://docs.djangoproject.com/en/1.8/ref/settings/#auth&gt;`_">https://docs.djangoproject.com/en/1.8/ref/settings/#auth&gt;`_</link>. On y trouve par exemple les paramètres suivants:</simpara>
<itemizedlist>
<listitem>
<simpara><literal>LOGIN_REDIRECT_URL</literal>: si vous ne spécifiez pas le paramètre <literal>next</literal>, l&#8217;utilisateur sera automatiquement redirigé vers cette page.</simpara>
</listitem>
<listitem>
<simpara><literal>LOGIN_URL</literal>: l&#8217;URL de connexion à utiliser. Par défaut, l&#8217;utilisateur doit se rendre sur la page <literal>/accounts/login</literal>.</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_social_authentification">
<title>Social-Authentification</title>
<simpara>Voir ici : <link xl:href="https://github.com/omab/python-social-auth">python social auth</link></simpara>
</section>
<section xml:id="_un_petit_mot_sur_oauth">
<title>Un petit mot sur OAuth</title>
<simpara>OAuth est un standard libre définissant un ensemble de méthodes à implémenter pour l&#8217;accès (l&#8217;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.</simpara>
<simpara>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 &lt;<link xl:href="http://en.wikipedia.org/wiki/OAuth&gt;`_">http://en.wikipedia.org/wiki/OAuth&gt;`_</link>.</simpara>
<simpara>Une introduction à OAuth est <link xl:href="http://hueniverse.com/oauth/guide/intro/">disponible ici</link>. Elle introduit le protocole comme étant une <literal>valet key</literal>, une clé que l&#8217;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&#8217;identifier une personne, tout en lui donnant un accès restreint à votre application.</simpara>
<simpara>L&#8217;utilisation de jetons permet notamment de définir une durée d&#8217;utilisation et une portée d&#8217;utilisation. L&#8217;utilisateur d&#8217;un service A peut par exemple autoriser un service B à accéder à des ressources qu&#8217;il possède, sans pour autant révéler son nom d&#8217;utilisateur ou son mot de passe.</simpara>
<simpara>L&#8217;exemple repris au niveau du <link xl:href="http://hueniverse.com/oauth/guide/workflow/">workflow</link> 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&#8217;ouvre pour l&#8217;utilisateur, et lui demande d&#8217;introduire sa "pièce d&#8217;identité". Le site A, ayant reçu une demande de B, mais certifiée par l&#8217;utilisateur, ouvre alors les ressources et lui permet d&#8217;y accéder.</simpara>
<programlisting language="python" linenumbering="unnumbered">INSTALLED_APPS = [
"django.contrib..."
]</programlisting>
<simpara>peut être splitté en plusieurs parties:</simpara>
<programlisting language="python" linenumbering="unnumbered">INSTALLED_APPS = [
]
THIRD_PARTIES = [
]
MY_APPS = [
]</programlisting>
</section>
</section>
</chapter>
<chapter xml:id="_context_processors">
<title><emphasis>Context Processors</emphasis></title>
<simpara>Mise en pratique: un <emphasis>context processor</emphasis> sert <emphasis>grosso-modo</emphasis> à peupler l&#8217;ensemble des données transmises des vues aux templates avec des données communes.
Un context processor est un peu l&#8217;équivalent d&#8217;un middleware, mais entre les données et les templates, là où le middleware va s&#8217;occuper des données relatives aux réponses et requêtes elles-mêmes.</simpara>
<programlisting language="python" linenumbering="unnumbered"># core/context_processors.py
import subprocess
def git_describe(request) -&gt; str:
return {
"git_describe": subprocess.check_output(
["git", "describe", "--always"]
).strip(),
"git_date": subprocess.check_output(
["git", "show", "-s", r"--format=%cd", r"--date=format:%d-%m-%Y"]
),
}</programlisting>
<simpara>Ceci aura pour effet d&#8217;ajouter les deux variables <literal>git_describe</literal> et <literal>git_date</literal> dans tous les contextes de tous les templates de l&#8217;application.</simpara>
<programlisting language="python" linenumbering="unnumbered">TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates"),],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"core.context_processors.git_describe"
],
},
},
]</programlisting>
<section xml:id="_tests">
<title>Tests</title>
<blockquote>
<attribution>
Robert C. Martin
<citetitle>Clean Architecture</citetitle>
</attribution>
<simpara>Tests are part of the system.</simpara>
</blockquote>
<section xml:id="_types_de_tests">
<title>Types de tests</title>
<simpara>Les <emphasis role="strong">tests unitaires</emphasis> ciblent typiquement une seule fonction, classe ou méthode, de manière isolée, en fournissant au développeur l&#8217;assurance que son code réalise ce qu&#8217;il en attend. Pour plusieurs raisons (et notamment en raison de performances), les tests unitaires utilisent souvent des données stubbées - pour éviter d&#8217;appeler le "vrai" service.</simpara>
<blockquote>
<simpara>The aim of a unit test is to show that a single part of the application does what programmer intends it to.</simpara>
</blockquote>
<simpara>Les <emphasis role="strong">tests d&#8217;acceptance</emphasis> vérifient que l&#8217;application fonctionne comme convenu, mais à un plus haut niveau (fonctionnement correct d&#8217;une API, validation d&#8217;une chaîne d&#8217;actions effectuées par un humain, &#8230;&#8203;).</simpara>
<blockquote>
<simpara>The objective of acceptance tests is to prove that our application does what the customer meant it to.</simpara>
</blockquote>
<simpara>Les <emphasis role="strong">tests d&#8217;intégration</emphasis> vérifient que l&#8217;application coopère correctement avec les systèmes périphériques.</simpara>
<simpara>De manière plus générale, si nous nous rendons compte que les tests sont trop compliqués à écrire ou nous coûtent trop de temps, c&#8217;est sans doute que l&#8217;architecture de la solution n&#8217;est pas adaptée et que les composants sont couplés les uns aux autres. Dans ces cas, il sera nécessaire de refactoriser le code, afin que chaque module puisse être testé indépendamment des autres. cite:[clean_architecture]</simpara>
<blockquote>
<attribution>
Robert C. Martin
<citetitle>Clean Architecture</citetitle>
</attribution>
<simpara>Martin Fowler observes that, in general, "a ten minute build [and test process] is perfectly within reason&#8230;&#8203;
[We first] do the compilation and run tests that are more localized unit tests with the database completely stubbed out.
Such tests can run very fast, keeping within the ten minutes guideline.
However any bugs that involve larger scale intercations, particularly those involving the real database, won&#8217;t be found.
The second stage build runs a different suite of tests [acceptance tests] that do hit the real database and involve more end-to-end behavior.
This suite may take a couple of hours to run.</simpara>
</blockquote>
<simpara>Au final, le plus important est de toujours corréler les phases de tests indépendantes du reste du travail (de développement, ici), en l&#8217;automatisant au plus près de sa source de création.</simpara>
<simpara>En résumé, il est recommandé de:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Tester que le nommage d&#8217;une URL (son attribut <literal>name</literal> dans les fichiers <literal>urls.py</literal>) corresponde à la fonction que l&#8217;on y a définie</simpara>
</listitem>
<listitem>
<simpara>Tester que l&#8217;URL envoie bien vers l&#8217;exécution d&#8217;une fonction (et que cette fonction est celle que l&#8217;on attend)</simpara>
</listitem>
</orderedlist>
<simpara>TODO: Voir comment configurer une <literal>memoryDB</literal> pour l&#8217;exécution des tests.</simpara>
</section>
<section xml:id="_tests_de_nommage">
<title>Tests de nommage</title>
<programlisting language="python" linenumbering="unnumbered">from django.core.urlresolvers import reverse
from django.test import TestCase
class HomeTests(TestCase):
def test_home_view_status_code(self):
url = reverse("home")
response = self.client.get(url)
self.assertEquals(response.status_code, 200)</programlisting>
</section>
<section xml:id="_tests_durls">
<title>Tests d&#8217;urls</title>
<programlisting language="python" linenumbering="unnumbered">from django.core.urlresolvers import reverse
from django.test import TestCase
from .views import home
class HomeTests(TestCase):
def test_home_view_status_code(self):
view = resolve("/")
self.assertEquals(view.func, home)</programlisting>
</section>
</section>
</chapter>
<chapter xml:id="_conclusions_2">
<title>Conclusions</title>
</chapter>
</part>
<part xml:id="_déploiement">
<title>Déploiement</title>
<partintro>
<blockquote>
<attribution>
Robert C. Martin
<citetitle>Clean Architecture</citetitle>
</attribution>
<simpara>To be effective, a software system must be deployable.
The higher the cost of deployements, the less useful the system is.
A goal of a software architecture, then, should be to make a system that can be easily deployed with a single action.
Unfortunately, deployment strategy is seldom considered during initial development.
This leads to architectures that may be make the system easy to develop, but leave it very difficult to deploy.</simpara>
</blockquote>
<simpara>Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local,
on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un serveur.
Il est du coup probable d&#8217;oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu&#8217;elles puissent être mises à disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé.
Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque.
Lobjectif est qu&#8217;il soit rapide et fiable.
Ceci peut être atteint au travers dun partitionnement correct, incluant le fait que le composant principal sassure que chaque sous-composant est correctement démarré intégré et supervisé.</simpara>
<simpara>Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d&#8217;installation, de mises à jour et de sauvegardes.
A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d&#8217;un tronc commun au développement cite:[devops_handbook].</simpara>
<simpara>Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux versions), puis redémarrer les services adéquats, et la procédure complète se résumera à quelques lignes d&#8217;un script bash.</simpara>
<blockquote>
<attribution>
DevOps Handbook
<citetitle>Introduction</citetitle>
</attribution>
<simpara>Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and disruptions such as service outages, service impairments, or security or compliance failures.</simpara>
</blockquote>
<simpara>Le serveur que django met à notre disposition <emphasis>via</emphasis> la commande <literal>runserver</literal> est extrêmement pratique, mais il est uniquement prévu pour la phase développement: en production, il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, &#8230;&#8203;).
De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l&#8217;on connait ses limites et que l&#8217;on se limite à un utilisateur à la fois.
En production, il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanés.
En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu&#8217;un autre processus a déjà pris la main pour écrire ses données.
En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou ou simplement parce que le serveur répondra trop lentement.</simpara>
<simpara>L&#8217;objectif de cette partie est de parcourir les différentes possibilités qui s&#8217;offrent à nous en termes de déploiement, tout en faisant en sorte que le code soit le moins couplé possible à sa destination de production.
L&#8217;objectif est donc de faire en sorte qu&#8217;une même application puisse être hébergées par plusieurs hôtes sans avoir à subir de modifications.
Nous vous renvoyons vers les 12-facteurs dont nous avons déjà parlé et qui vous énormément nous aider, puisque ce sont des variables d&#8217;environnement qui vont réellement piloter le câblage entre l&#8217;application, ses composants et son hébergeur.</simpara>
<simpara>RedHat proposait récemment un article intitulé <emphasis>*What Is IaaS*</emphasis>, qui présentait les principales différences entre types d&#8217;hébergement.</simpara>
<figure>
<title>L&#8217;infrastructure en tant que service, cc. <emphasis>RedHat Cloud Computing</emphasis></title>
<mediaobject>
<imageobject>
<imagedata fileref="images/deployment/iaas_focus-paas-saas-diagram.png"/>
</imageobject>
<textobject><phrase>iaas focus paas saas diagram</phrase></textobject>
</mediaobject>
</figure>
<simpara>Ainsi, on trouve:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Le déploiment <emphasis>on-premises</emphasis> ou <emphasis>on-site</emphasis></simpara>
</listitem>
<listitem>
<simpara>Les <emphasis>Infrastructures as a service</emphasis> ou <emphasis><indexterm>
<primary>IaaS</primary>
</indexterm>IaaS</emphasis></simpara>
</listitem>
<listitem>
<simpara>Les <emphasis>Platforms as a service</emphasis> ou <emphasis><indexterm>
<primary>PaaS</primary>
</indexterm>PaaS</emphasis></simpara>
</listitem>
<listitem>
<simpara>Les <emphasis>Softwares as a service</emphasis> ou <emphasis><indexterm>
<primary>SaaS</primary>
</indexterm>SaaS</emphasis>, ce dernier point nous concernant moins, puisque c&#8217;est nous qui développons le logiciel.</simpara>
</listitem>
</orderedlist>
<simpara>Dans cette partie, nous aborderons les points suivants:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Définir l&#8217;infrastructure et les composants nécessaires à notre application</simpara>
</listitem>
<listitem>
<simpara>Configurer l&#8217;hôte qui hébergera l&#8217;application et y déployer notre application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt. A ce stade, nous aurons déjà une application disponible.</simpara>
</listitem>
<listitem>
<simpara>Configurer les outils nécessaires à la bonne exécution de ce code et de ses fonctionnalités: les différentes méthodes de supervision de l&#8217;application, comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter correctement l&#8217;information.</simpara>
</listitem>
</orderedlist>
</partintro>
<chapter xml:id="_infrastructure_composants">
<title>Infrastructure &amp; composants</title>
<simpara>Pour une mise ne production, le standard <emphasis>de facto</emphasis> est le suivant:</simpara>
<itemizedlist>
<listitem>
<simpara>Nginx comme reverse proxy</simpara>
</listitem>
<listitem>
<simpara>HAProxy pour la distribution de charge</simpara>
</listitem>
<listitem>
<simpara>Gunicorn ou Uvicorn comme serveur d&#8217;application</simpara>
</listitem>
<listitem>
<simpara>Supervisor pour le monitoring</simpara>
</listitem>
<listitem>
<simpara>PostgreSQL ou MySQL/MariaDB comme bases de données.</simpara>
</listitem>
<listitem>
<simpara>Celery et RabbitMQ pour l&#8217;exécution de tâches asynchrones</simpara>
</listitem>
<listitem>
<simpara>Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).</simpara>
</listitem>
<listitem>
<simpara>Sentry, pour le suivi des bugs</simpara>
</listitem>
</itemizedlist>
<simpara>Si nous schématisons l&#8217;infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>L&#8217;utilisateur fait une requête via son navigateur (Firefox ou Chrome)</simpara>
</listitem>
<listitem>
<simpara>Le navigateur envoie une requête http, sa version, un verbe (GET, POST, &#8230;&#8203;), un port et éventuellement du contenu</simpara>
</listitem>
<listitem>
<simpara>Le firewall du serveur (Debian GNU/Linux, CentOS, &#8230;&#8203;) vérifie si la requête peut être prise en compte</simpara>
</listitem>
<listitem>
<simpara>La requête est transmise à l&#8217;application qui écoute sur le port (probablement 80 ou 443; et <emphasis>a priori</emphasis> Nginx)</simpara>
</listitem>
<listitem>
<simpara>Elle est ensuite transmise par socket et est prise en compte par un des <emphasis>workers</emphasis> (= un processus Python) instancié par Gunicorn. Si l&#8217;un de ces travailleurs venait à planter, il serait automatiquement réinstancié par Supervisord.</simpara>
</listitem>
<listitem>
<simpara>Qui la transmet ensuite à l&#8217;un de ses <emphasis>workers</emphasis> (= un processus Python).</simpara>
</listitem>
<listitem>
<simpara>Après exécution, une réponse est renvoyée à l&#8217;utilisateur.</simpara>
</listitem>
</orderedlist>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/diagrams/architecture.png"/>
</imageobject>
<textobject><phrase>architecture</phrase></textobject>
</mediaobject>
</informalfigure>
<section xml:id="_reverse_proxy">
<title>Reverse proxy</title>
<simpara>Le principe du <emphasis role="strong">proxy inverse</emphasis> est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système.
Il serait tout à fait possible de rendre notre application directement accessible depuis l&#8217;extérieur, mais le proxy a aussi l&#8217;intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats <footnote><simpara><link xl:href="https://fr.wikipedia.org/wiki/Proxy_inverse">https://fr.wikipedia.org/wiki/Proxy_inverse</link></simpara></footnote></simpara>
</section>
<section xml:id="_load_balancer">
<title>Load balancer</title>
</section>
<section xml:id="_workers">
<title>Workers</title>
</section>
<section xml:id="_supervision_des_processus">
<title>Supervision des processus</title>
</section>
<section xml:id="_base_de_données_2">
<title>Base de données</title>
</section>
<section xml:id="_tâches_asynchrones">
<title>Tâches asynchrones</title>
</section>
<section xml:id="_mise_en_cache">
<title>Mise en cache</title>
</section>
</chapter>
<chapter xml:id="_code_source">
<title>Code source</title>
<simpara>Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>effectuer le routage des données,</simpara>
</listitem>
<listitem>
<simpara>trouver la bonne fonction à exécuter,</simpara>
</listitem>
<listitem>
<simpara>récupérer les données depuis la base de données,</simpara>
</listitem>
<listitem>
<simpara>effectuer le rendu ou la conversion des données,</simpara>
</listitem>
<listitem>
<simpara>et renvoyer une réponse à l&#8217;utilisateur.</simpara>
</listitem>
</orderedlist>
<simpara>Comme nous l&#8217;avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d&#8217;une application.
Il est possible de démarrer petit, et de suivre l&#8217;évolution des besoins en fonction de la charge estimée ou ressentie, d&#8217;ajouter un mécanisme de mise en cache, des logiciels de suivi, &#8230;&#8203;</simpara>
</chapter>
<chapter xml:id="_outils_de_supervision_et_de_mise_à_disposition">
<title>Outils de supervision et de mise à disposition</title>
<section xml:id="_logs">
<title>Logs</title>
</section>
</chapter>
<chapter xml:id="_logging">
<title>Logging</title>
<simpara>La structure des niveaux de journaux est essentielle.</simpara>
<blockquote>
<attribution>
Dan North
<citetitle>former ToughtWorks consultant</citetitle>
</attribution>
<simpara>When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m. Low printer toner is not an ERROR.</simpara>
</blockquote>
<itemizedlist>
<listitem>
<simpara><emphasis role="strong">DEBUG</emphasis>: Il s&#8217;agit des informations qui concernent tout ce qui peut se passer durant l&#8217;exécution de l&#8217;application. Généralement, ce niveau est désactivé pour une application qui passe en production, sauf s&#8217;il est nécessaire d&#8217;isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">INFO</emphasis>: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, &#8230;&#8203;</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">WARN</emphasis>: Regroupe les informations qui pourraient potentiellement devenir des erreurs.</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">ERROR</emphasis>: Indique les informations internes - Erreur lors de l&#8217;appel d&#8217;une API, erreur interne, &#8230;&#8203;</simpara>
</listitem>
<listitem>
<simpara><emphasis role="strong">FATAL</emphasis> (ou <emphasis role="strong">EXCEPTION</emphasis>): &#8230;&#8203; généralement suivie d&#8217;une terminaison du programme ;-) - Bind raté d&#8217;un socket, etc.</simpara>
</listitem>
</itemizedlist>
<simpara>La configuration des <emphasis>loggers</emphasis> est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore.
Il est ainsi possible de définir des formattages, gestionnaires (<emphasis>handlers</emphasis>) et loggers distincts, en fonction de nos applications.</simpara>
<simpara>Sauf que comme nous l&#8217;avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d&#8217;évènements.
Il n&#8217;est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les <literal>StreamHandler</literal>.
La configuration que nous allons utiliser est celle-ci:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Formattage: à définir - mais la variante suivante est complète, lisible et pratique: <literal>{levelname} {asctime} {module} {process:d} {thread:d} {message}</literal></simpara>
</listitem>
<listitem>
<simpara>Handler: juste un, qui définit un <literal>StreamHandler</literal></simpara>
</listitem>
<listitem>
<simpara>Logger: pour celui-ci, nous avons besoin d&#8217;un niveau (<literal>level</literal>) et de savoir s&#8217;il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de <literal>propagate</literal> à <literal>True</literal>.</simpara>
</listitem>
</orderedlist>
<programlisting language="python" linenumbering="unnumbered">LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
},
'simple': {
'format': '{levelname} {asctime} {module} {message}',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': "verbose"
}
},
'loggers': {
'khana': {
'handlers': ['console'],
'level': env("LOG_LEVEL", default="DEBUG"),
'propagate': True,
},
}
}</programlisting>
<simpara>Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant:</simpara>
<programlisting language="python" linenumbering="unnumbered">import logging
logger = logging.getLogger(__name__)
logger.debug('helloworld')</programlisting>
<simpara><link xl:href="https://docs.djangoproject.com/en/stable/topics/logging/#examples">Par exemples</link>.</simpara>
<section xml:id="_logging_2">
<title>Logging</title>
<orderedlist numeration="arabic">
<listitem>
<simpara>Sentry via sentry_sdk</simpara>
</listitem>
<listitem>
<simpara>Nagios</simpara>
</listitem>
<listitem>
<simpara>LibreNMS</simpara>
</listitem>
<listitem>
<simpara>Zabbix</simpara>
</listitem>
</orderedlist>
<simpara>Il existe également <link xl:href="https://munin-monitoring.org">Munin</link>, <link xl:href="https://www.elastic.co">Logstash, ElasticSearch et Kibana (ELK-Stack)</link> ou <link xl:href="https://www.fluentd.org">Fluentd</link>.</simpara>
</section>
</chapter>
<chapter xml:id="_méthode_de_déploiement">
<title>Méthode de déploiement</title>
<simpara>Nous allons détailler ci-dessous trois méthodes de déploiement:</simpara>
<itemizedlist>
<listitem>
<simpara>Sur une machine hôte, en embarquant tous les composants sur un même serveur. Ce ne sera pas idéal, puisqu&#8217;il ne sera pas possible de configurer un <emphasis>load balancer</emphasis>, de routeur plusieurs basées de données, mais ce sera le premier cas de figure.</simpara>
</listitem>
<listitem>
<simpara>Dans des containers, avec Docker-Compose.</simpara>
</listitem>
<listitem>
<simpara>Sur une <emphasis role="strong">Plateforme en tant que Service</emphasis> (ou plus simplement, <emphasis role="strong"><indexterm>
<primary>PaaS</primary>
</indexterm>PaaS</emphasis>), pour faire abstraction de toute la couche de configuration du serveur.</simpara>
</listitem>
</itemizedlist>
</chapter>
<chapter xml:id="_déploiement_sur_debian">
<title>Déploiement sur Debian</title>
<simpara>La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits. Il est faut absolument éviter de faire tourner une application en tant qu&#8217;utilisateur <emphasis role="strong">root</emphasis>, car la moindre faille pourrait avoir des conséquences catastrophiques.</simpara>
<simpara>Une fois que ces utilisateurs seront configurés, nous pourrons passer à l&#8217;étape de configuration, qui consistera à:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Déployer les sources</simpara>
</listitem>
<listitem>
<simpara>Démarrer un serveur implémentant une interface WSGI (<emphasis role="strong">Web Server Gateway Interface</emphasis>), qui sera chargé de créer autant de <phrase role="line-through">petits lutins</phrase> travailleurs que nous le désirerons.</simpara>
</listitem>
<listitem>
<simpara>Démarrer un superviseur, qui se chargera de veiller à la bonne santé de nos petits travailleurs, et en créer de nouveaux s&#8217;il le juge nécessaire</simpara>
</listitem>
<listitem>
<simpara>Configurer un proxy inverse, qui s&#8217;occupera d&#8217;envoyer les requêtes d&#8217;un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l&#8217;un des travailleurs.</simpara>
</listitem>
</orderedlist>
<simpara>La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, &#8230;&#8203; Il existe des dizaines d&#8217;hébergements typés VPS (<emphasis role="strong">Virtual Private Server</emphasis>). A vous de choisir celui qui vous convient <footnote><simpara>Personnellement, j&#8217;ai un petit faible pour Hetzner Cloud</simpara></footnote>.</simpara>
<programlisting language="bash" linenumbering="unnumbered">apt update
groupadd --system webapps <co xml:id="CO15-1"/>
groupadd --system gunicorn_sockets <co xml:id="CO15-2"/>
useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift <co xml:id="CO15-3"/>
mkdir -p /home/gwift <co xml:id="CO15-4"/>
chown gwift:webapps /home/gwift <co xml:id="CO15-5"/></programlisting>
<calloutlist>
<callout arearefs="CO15-1">
<para>On ajoute un groupe intitulé <literal>webapps</literal></para>
</callout>
<callout arearefs="CO15-2">
<para>On crée un groupe pour les communications via sockets</para>
</callout>
<callout arearefs="CO15-3">
<para>On crée notre utilisateur applicatif; ses applications seront placées dans le répertoire <literal>/home/gwift</literal></para>
</callout>
<callout arearefs="CO15-4">
<para>On crée le répertoire home/gwift</para>
</callout>
<callout arearefs="CO15-5">
<para>On donne les droits sur le répertoire /home/gwift</para>
</callout>
</calloutlist>
<section xml:id="_installation_des_dépendances_systèmes">
<title>Installation des dépendances systèmes</title>
<simpara>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&#8217;installer en parallèle de la version officiellement supportée par votre distribution.</simpara>
<simpara>Pour CentOS, vous avez donc deux possibilités :</simpara>
<programlisting language="bash" linenumbering="unnumbered">yum install python36 -y</programlisting>
<simpara>Ou passer par une installation alternative:</simpara>
<programlisting language="bash" linenumbering="unnumbered">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 <co xml:id="CO16-1"/></programlisting>
<calloutlist>
<callout arearefs="CO16-1">
<para><emphasis role="strong">Attention !</emphasis> Le paramètre <literal>altinstall</literal> est primordial. Sans lui, vous écraserez l&#8217;interpréteur initialement supporté par la distribution, et cela pourrait avoir des effets de bord non souhaités.</para>
</callout>
</calloutlist>
</section>
<section xml:id="_installation_de_la_base_de_données">
<title>Installation de la base de données</title>
<simpara>On l&#8217;a déjà vu, Django se base sur un pattern type <link xl:href="https://www.martinfowler.com/eaaCatalog/activeRecord.html">ActiveRecords</link> pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données connus:</simpara>
<itemizedlist>
<listitem>
<simpara>SQLite (en natif, mais Django 3.0 exige une version du moteur supérieure ou égale à la 3.8)</simpara>
</listitem>
<listitem>
<simpara>MariaDB (en natif depuis Django 3.0),</simpara>
</listitem>
<listitem>
<simpara>PostgreSQL au travers de psycopg2 (en natif aussi),</simpara>
</listitem>
<listitem>
<simpara>Microsoft SQLServer grâce aux drivers [&#8230;&#8203;à compléter]</simpara>
</listitem>
<listitem>
<simpara>Oracle via <link xl:href="https://oracle.github.io/python-cx_Oracle/">cx_Oracle</link>.</simpara>
</listitem>
</itemizedlist>
<caution>
<simpara>Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n&#8217;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&#8217;un Oracle 12.1).</simpara>
</caution>
<simpara>Ci-dessous, quelques procédures d&#8217;installation pour mettre un serveur à disposition. Les deux plus simples seront MariaDB et PostgreSQL, qu&#8217;on couvrira ci-dessous. Oracle et Microsoft SQLServer se trouveront en annexes.</simpara>
<section xml:id="_postgresql">
<title>PostgreSQL</title>
<simpara>On commence par installer PostgreSQL.</simpara>
<simpara>Par exemple, dans le cas de debian, on exécute la commande suivante:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$$$ aptitude install postgresql postgresql-contrib</programlisting>
<simpara>Ensuite, on crée un utilisateur pour la DB:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$$$ su - postgres
postgres@gwift:~$ createuser --interactive -P
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
postgres@gwift:~$</programlisting>
<simpara>Finalement, on peut créer la DB:</simpara>
<programlisting language="bash" linenumbering="unnumbered">postgres@gwift:~$ createdb --owner gwift_user gwift
postgres@gwift:~$ exit
logout
$$$</programlisting>
<note>
<simpara>penser à inclure un bidule pour les backups.</simpara>
</note>
</section>
<section xml:id="_mariadb">
<title>MariaDB</title>
<simpara>Idem, installation, configuration, backup, tout ça.
A copier de grimboite, je suis sûr d&#8217;avoir des notes là-dessus.</simpara>
</section>
<section xml:id="_microsoft_sql_server">
<title>Microsoft SQL Server</title>
</section>
<section xml:id="_oracle">
<title>Oracle</title>
</section>
</section>
<section xml:id="_préparation_de_lenvironnement_utilisateur">
<title>Préparation de l&#8217;environnement utilisateur</title>
<programlisting language="bash" linenumbering="unnumbered">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 ...</programlisting>
<simpara>La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.</simpara>
<simpara>A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes suivantes:</simpara>
<programlisting language="bash" linenumbering="unnumbered"># 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</programlisting>
</section>
<section xml:id="_configuration_de_lapplication">
<title>Configuration de l&#8217;application</title>
<programlisting language="bash" linenumbering="unnumbered">SECRET_KEY=&lt;set your secret key here&gt; <co xml:id="CO17-1"/>
ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/gwift/static
DATABASE= <co xml:id="CO17-2"/></programlisting>
<calloutlist>
<callout arearefs="CO17-1">
<para>La variable <literal>SECRET_KEY</literal> est notamment utilisée pour le chiffrement des sessions.</para>
</callout>
<callout arearefs="CO17-2">
<para>On fait confiance à django_environ pour traduire la chaîne de connexion à la base de données.</para>
</callout>
</calloutlist>
</section>
<section xml:id="_création_des_répertoires_de_logs">
<title>Création des répertoires de logs</title>
<programlisting language="text" linenumbering="unnumbered">mkdir -p /var/www/gwift/static</programlisting>
</section>
<section xml:id="_création_du_répertoire_pour_le_socket">
<title>Création du répertoire pour le socket</title>
<simpara>Dans le fichier <literal>/etc/tmpfiles.d/gwift.conf</literal>:</simpara>
<programlisting language="text" linenumbering="unnumbered">D /var/run/webapps 0775 gwift gunicorn_sockets -</programlisting>
<simpara>Suivi de la création par systemd :</simpara>
<programlisting language="text" linenumbering="unnumbered">systemd-tmpfiles --create</programlisting>
</section>
<section xml:id="_gunicorn">
<title>Gunicorn</title>
<programlisting language="bash" linenumbering="unnumbered">#!/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=-</programlisting>
</section>
<section xml:id="_supervision_keep_alive_et_autoreload">
<title>Supervision, keep-alive et autoreload</title>
<simpara>Pour la supervision, on passe par Supervisor. Il existe d&#8217;autres superviseurs,</simpara>
<programlisting language="bash" linenumbering="unnumbered">yum install supervisor -y</programlisting>
<simpara>On crée ensuite le fichier <literal>/etc/supervisord.d/gwift.ini</literal>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">[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</programlisting>
<simpara>Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu&#8217;il tourne correctement:</simpara>
<programlisting language="bash" linenumbering="unnumbered">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/</programlisting>
<simpara>On peut aussi vérifier que l&#8217;application est en train de tourner, à l&#8217;aide de la commande <literal>supervisorctl</literal>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$$$ supervisorctl status gwift
gwift RUNNING pid 31983, uptime 0:01:00</programlisting>
<simpara>Et pour gérer le démarrage ou l&#8217;arrêt, on peut passer par les commandes suivantes:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$$$ 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</programlisting>
</section>
<section xml:id="_configuration_du_firewall_et_ouverture_des_ports">
<title>Configuration du firewall et ouverture des ports</title>
<literallayout class="monospaced">et 443 (HTTPS).</literallayout>
<programlisting language="text" linenumbering="unnumbered">firewall-cmd --permanent --zone=public --add-service=http <co xml:id="CO18-1"/>
firewall-cmd --permanent --zone=public --add-service=https <co xml:id="CO18-2"/>
firewall-cmd --reload</programlisting>
<calloutlist>
<callout arearefs="CO18-1">
<para>On ouvre le port 80, uniquement pour autoriser une connexion HTTP, mais qui sera immédiatement redirigée vers HTTPS</para>
</callout>
<callout arearefs="CO18-2">
<para>Et le port 443 (forcément).</para>
</callout>
</calloutlist>
</section>
<section xml:id="_installation_dnginx">
<title>Installation d&#8217;Nginx</title>
<screen linenumbering="unnumbered">yum install nginx -y
usermod -a -G gunicorn_sockets nginx</screen>
<simpara>On configure ensuite le fichier <literal>/etc/nginx/conf.d/gwift.conf</literal>:</simpara>
<screen>upstream gwift_app {
server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0;
}
server {
listen 80;
server_name &lt;server_name&gt;;
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/ { <co xml:id="CO19-1"/>
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; <co xml:id="CO19-2"/>
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_app;
}
}</screen>
<calloutlist>
<callout arearefs="CO19-1">
<para>Ce répertoire sera complété par la commande <literal>collectstatic</literal> que l&#8217;on verra plus tard. L&#8217;objectif est que les fichiers ne demandant aucune intelligence soit directement servis par Nginx. Cela évite d&#8217;avoir un processus Python (relativement lent) qui doive être instancié pour servir un simple fichier statique.</para>
</callout>
<callout arearefs="CO19-2">
<para>Afin d&#8217;éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1</para>
</callout>
</calloutlist>
</section>
<section xml:id="_mise_à_jour">
<title>Mise à jour</title>
<simpara>Script de mise à jour.</simpara>
<programlisting language="bash" linenumbering="unnumbered">su - &lt;user&gt;
source ~/.venvs/&lt;app&gt;/bin/activate
cd ~/webapps/&lt;app&gt;
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
kill -HUP `ps -C gunicorn fch -o pid | head -n 1` <co xml:id="CO20-1"/></programlisting>
<calloutlist>
<callout arearefs="CO20-1">
<para><link xl:href="https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file">https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file</link></para>
</callout>
</calloutlist>
</section>
<section xml:id="_configuration_des_sauvegardes">
<title>Configuration des sauvegardes</title>
<simpara>Les sauvegardes ont été configurées avec borg: <literal>yum install borgbackup</literal>.</simpara>
<simpara>C&#8217;est l&#8217;utilisateur gwift qui s&#8217;en occupe.</simpara>
<screen>mkdir -p /home/gwift/borg-backups/
cd /home/gwift/borg-backups/
borg init gwift.borg -e=none
borg create gwift.borg::{now} ~/bin ~/webapps</screen>
<simpara>Et dans le fichier crontab :</simpara>
<screen>0 23 * * * /home/gwift/bin/backup.sh</screen>
</section>
<section xml:id="_rotation_des_jounaux">
<title>Rotation des jounaux</title>
<programlisting language="bash" linenumbering="unnumbered">/var/log/gwift/* {
weekly
rotate 3
size 10M
compress
delaycompress
}</programlisting>
<simpara>Puis on démarre logrotate avec # logrotate -d /etc/logrotate.d/gwift pour vérifier que cela fonctionne correctement.</simpara>
</section>
<section xml:id="_ansible">
<title>Ansible</title>
<simpara>TODO</simpara>
</section>
</chapter>
<chapter xml:id="_déploiement_sur_heroku">
<title>Déploiement sur Heroku</title>
<simpara><link xl:href="https://www.heroku.com">Heroku</link> est une <emphasis>Plateform As A Service</emphasis> <indexterm>
<primary>paas</primary>
</indexterm>, où vous choisissez le <emphasis>service</emphasis> dont vous avez besoin (une base de données, un service de cache, un service applicatif, &#8230;&#8203;), vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l&#8217;hôte.
Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d&#8217;autant plus qu&#8217;il ne sera pas possible de modifier un fichier une fois qu&#8217;elle aura démarré: si vous souhaitez modifier un paramètre, cela reviendra à couper l&#8217;actuelle et envoyer de nouveaux paramètres et recommencer le déploiement depuis le début.</simpara>
<figure>
<title>Invest in apps, not ops. Heroku handles the hard stuff — patching and upgrading, 24/7 ops and security, build systems, failovers, and more — so your developers can stay focused on building great apps.</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/deployment/heroku.png"/>
</imageobject>
<textobject><phrase>heroku</phrase></textobject>
</mediaobject>
</figure>
<simpara>Pour un projet de type "hobby" et pour l&#8217;exemple de déploiement ci-dessous, il est tout à fait possible de s&#8217;en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un <emphasis>Most Valuable Product</emphasis> en place.
La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un <emphasis>bucket compatible S3</emphasis>, par exemple chez Amazon, Scaleway ou OVH.</simpara>
<simpara>Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé.
Il suffit donc d&#8217;envoyer les sources de votre application vers ce dépôt pour qu&#8217;Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition.
Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l&#8217;équipe.
Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de <emphasis>pipeline</emphasis>, lorsque tous les tests unitaires et d&#8217;intégration auront été réalisés.</simpara>
<simpara>Au travers de la commande <literal>heroku create</literal>, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier <literal>.git/config</literal> ci-dessous:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ heroku create
Creating app... done, ⬢ young-temple-86098
https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-temple-86098.git
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "heroku"]
url = https://git.heroku.com/still-thicket-66406.git
fetch = +refs/heads/*:refs/remotes/heroku/*</programlisting>
<simpara>IMPORTANT:</simpara>
<screen>Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration.
Celle-ci se limite aux deux fichiers suivants:
* Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI
* Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`)</screen>
<simpara>Après ce paramétrage, il suffit de pousser les changements vers ce nouveau dépôt grâce à la commande <literal>git push heroku master</literal>.</simpara>
<warning>
<simpara>Heroku propose des espaces de déploiements, mais pas d&#8217;espace de stockage.
Il est possible d&#8217;y envoyer des fichiers utilisateurs (typiquement, des media personnalisés), mais ceux-ci seront perdus lors du redémarrage du container.
Il est donc primordial de configurer correctement l&#8217;hébergement des fichiers média, de préférences sur un stockage compatible S3. <indexterm>
<primary>s3</primary>
</indexterm></simpara>
</warning>
<simpara>Prêt à vous lancer ? Commencez par créer un compte: <link xl:href="https://signup.heroku.com/python">https://signup.heroku.com/python</link>.</simpara>
<section xml:id="_configuration_du_compte_heroku">
<title>Configuration du compte Heroku</title>
<simpara>+ Récupération des valeurs d&#8217;environnement pour les réutiliser ci-dessous.</simpara>
<simpara>Vous aurez peut-être besoin d&#8217;un coup de pouce pour démarrer votre première application; heureusement, la documentation est super bien faite:</simpara>
<figure>
<title>Heroku: Commencer à travailler avec un langage</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/deployment/heroku-new-app.png"/>
</imageobject>
<textobject><phrase>heroku new app</phrase></textobject>
</mediaobject>
</figure>
<simpara>Installez ensuite la CLI (<emphasis>Command Line Interface</emphasis>) en suivant <link xl:href="https://devcenter.heroku.com/articles/heroku-cli">la documentation suivante</link>.</simpara>
<simpara>Au besoin, cette CLI existe pour:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>macOS, <emphasis>via</emphasis> `brew `</simpara>
</listitem>
<listitem>
<simpara>Windows, grâce à un <link xl:href="https://cli-assets.heroku.com/heroku-x64.exe">binaire x64</link> (la version 32 bits existe aussi, mais il est peu probable que vous en ayez besoin)</simpara>
</listitem>
<listitem>
<simpara>GNU/Linux, via un script Shell <literal>curl <link xl:href="https://cli-assets.heroku.com/install.sh">https://cli-assets.heroku.com/install.sh</link> | sh</literal> ou sur <link xl:href="https://snapcraft.io/heroku">SnapCraft</link>.</simpara>
</listitem>
</orderedlist>
<simpara>Une fois installée, connectez-vous:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ heroku login</programlisting>
<simpara>Et créer votre application:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ heroku create
Creating app... done, ⬢ young-temple-86098
https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young-temple-86098.git</programlisting>
<figure>
<title>Notre application est à présent configurée!</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/deployment/heroku-app-created.png"/>
</imageobject>
<textobject><phrase>heroku app created</phrase></textobject>
</mediaobject>
</figure>
<simpara>Ajoutons lui une base de données, que nous sauvegarderons à intervalle régulier:</simpara>
<programlisting language="bash" linenumbering="unnumbered">$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on ⬢ still-thicket-66406... free
Database has been created and is available
! This database is empty. If upgrading, you can transfer
! data from another database with pg:copy
Created postgresql-clear-39693 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation
$ heroku pg:backups schedule --at '14:00 Europe/Brussels' DATABASE_URL
Scheduling automatic daily backups of postgresql-clear-39693 at 14:00 Europe/Brussels... done</programlisting>
<simpara>TODO: voir comment récupérer le backup de la db :-p</simpara>
<programlisting language="bash" linenumbering="unnumbered"># Copié/collé de https://cookiecutter-django.readthedocs.io/en/latest/deployment-on-heroku.html
heroku create --buildpack https://github.com/heroku/heroku-buildpack-python
heroku addons:create heroku-redis:hobby-dev
heroku addons:create mailgun:starter
heroku config:set PYTHONHASHSEED=random
heroku config:set WEB_CONCURRENCY=4
heroku config:set DJANGO_DEBUG=False
heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production
heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)"
# Generating a 32 character-long random string without any of the visually similar characters "IOl01":
heroku config:set DJANGO_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A-HJ-NP-Za-km-z2-9' | head -c 32)/"
# Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com'
heroku config:set DJANGO_ALLOWED_HOSTS=
# Assign with AWS_ACCESS_KEY_ID
heroku config:set DJANGO_AWS_ACCESS_KEY_ID=
# Assign with AWS_SECRET_ACCESS_KEY
heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY=
# Assign with AWS_STORAGE_BUCKET_NAME
heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME=
git push heroku master
heroku run python manage.py createsuperuser
heroku run python manage.py check --deploy
heroku open</programlisting>
</section>
<section xml:id="_configuration">
<title>Configuration</title>
<simpara>Pour qu&#8217;Heroku comprenne le type d&#8217;application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement.
Pour un projet Django, cela comprend, à placer à la racine de votre projet:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Un fichier <literal>requirements.txt</literal> (qui peut éventuellement faire appel à un autre fichier, <emphasis role="strong">via</emphasis> l&#8217;argument <literal>-r</literal>)</simpara>
</listitem>
<listitem>
<simpara>Un fichier <literal>Procfile</literal> ([sans extension](<link xl:href="https://devcenter.heroku.com/articles/procfile)">https://devcenter.heroku.com/articles/procfile)</link>!), qui expliquera la commande pour le protocole WSGI.</simpara>
</listitem>
</orderedlist>
<simpara>Dans notre exemple:</simpara>
<screen linenumbering="unnumbered"># requirements.txt
django==3.2.8
gunicorn
boto3
django-storages</screen>
<programlisting language="bash" linenumbering="unnumbered"># Procfile
release: python3 manage.py migrate
web: gunicorn gwift.wsgi</programlisting>
</section>
<section xml:id="_hébergement_s3">
<title>Hébergement S3</title>
<simpara>Pour cette partie, nous allons nous baser sur l&#8217;<link xl:href="https://www.scaleway.com/en/object-storage/">Object Storage de Scaleway</link>.
Ils offrent 75GB de stockage et de transfert par mois, ce qui va nous laisser suffisament d&#8217;espace pour jouer un peu 😉.</simpara>
<simpara><inlinemediaobject>
<imageobject>
<imagedata fileref="images/deployment/scaleway-object-storage-bucket.png"/>
</imageobject>
<textobject><phrase>scaleway object storage bucket</phrase></textobject>
</inlinemediaobject></simpara>
<simpara>L&#8217;idée est qu&#8217;au moment de la construction des fichiers statiques, Django aille simplement les héberger sur un espace de stockage compatible S3.
La complexité va être de configurer correctement les différents points de terminaison.
Pour héberger nos fichiers sur notre <emphasis role="strong">bucket</emphasis> S3, il va falloir suivre et appliquer quelques étapes dans l&#8217;ordre:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y en a - <emphasis role="strong">littéralement</emphasis> - des dizaines.</simpara>
</listitem>
<listitem>
<simpara>Ajouter la librairie <literal>boto3</literal>, qui s&#8217;occupera de "parler" avec ce type de protocole</simpara>
</listitem>
<listitem>
<simpara>Ajouter la librairie <literal>django-storage</literal>, qui va elle s&#8217;occuper de faire le câblage entre le fournisseur (<emphasis role="strong">via</emphasis> <literal>boto3</literal>) et Django, qui s&#8217;attend à ce qu&#8217;on lui donne un moteur de gestion <emphasis role="strong">via</emphasis> la clé [<literal>DJANGO_STATICFILES_STORAGE</literal>](<link xl:href="https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-STATICFILES_STORAGE">https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-STATICFILES_STORAGE</link>).</simpara>
</listitem>
</orderedlist>
<simpara>La première étape consiste à se rendre dans [la console Scaleway](<link xl:href="https://console.scaleway.com/project/credentials">https://console.scaleway.com/project/credentials</link>), pour gérer ses identifiants et créer un jeton.</simpara>
<simpara><inlinemediaobject>
<imageobject>
<imagedata fileref="images/deployment/scaleway-api-key.png"/>
</imageobject>
<textobject><phrase>scaleway api key</phrase></textobject>
</inlinemediaobject></simpara>
<simpara>Selon la documentation de <link xl:href="https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings">django-storages</link>, de <link xl:href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html">boto3</link> et de <link xl:href="https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/">Scaleway</link>, vous aurez besoin des clés suivantes au niveau du fichier <literal>settings.py</literal>:</simpara>
<programlisting language="python" linenumbering="unnumbered">AWS_ACCESS_KEY_ID = os.getenv('ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME')
AWS_DEFAULT_ACL = 'public-read'
AWS_LOCATION = 'static'
AWS_S3_SIGNATURE_VERSION = 's3v4'
AWS_S3_HOST = 's3.%s.scw.cloud' % (AWS_S3_REGION_NAME,)
AWS_S3_ENDPOINT_URL = 'https://%s' % (AWS_S3_HOST, )
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3ManifestStaticStorage'
STATIC_URL = '%s/%s/' % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)
# General optimization for faster delivery
AWS_IS_GZIPPED = True
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}</programlisting>
<simpara>Configurez-les dans la console d&#8217;administration d&#8217;Heroku:</simpara>
<simpara><inlinemediaobject>
<imageobject>
<imagedata fileref="images/deployment/heroku-vars-reveal.png"/>
</imageobject>
<textobject><phrase>heroku vars reveal</phrase></textobject>
</inlinemediaobject></simpara>
<simpara>Lors de la publication, vous devriez à présent avoir la sortie suivante, qui sera confirmée par le <emphasis role="strong">bucket</emphasis>:</simpara>
<programlisting language="bash" linenumbering="unnumbered">remote: -----&gt; $ python manage.py collectstatic --noinput
remote: 128 static files copied, 156 post-processed.</programlisting>
<simpara><inlinemediaobject>
<imageobject>
<imagedata fileref="images/deployment/gwift-cloud-s3.png"/>
</imageobject>
<textobject><phrase>gwift cloud s3</phrase></textobject>
</inlinemediaobject></simpara>
<simpara>Sources complémentaires:</simpara>
<itemizedlist>
<listitem>
<simpara>[How to store Django static and media files on S3 in production](<link xl:href="https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/">https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/</link>)</simpara>
</listitem>
<listitem>
<simpara>[Using Django and Boto3](<link xl:href="https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/">https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/</link>)</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_docker_compose">
<title>Docker-Compose</title>
<simpara>(c/c Ced' - 2020-01-24)</simpara>
<simpara>Ça y est, j&#8217;ai fait un test sur mon portable avec docker et cookiecutter pour django.</simpara>
<simpara>D&#8217;abords, après avoir installer docker-compose et les dépendances sous debian, tu dois t&#8217;ajouter dans le groupe docker, sinon il faut être root pour utiliser docker.
Ensuite, j&#8217;ai relancé mon pc car juste relancé un shell n&#8217;a pas suffit pour que je puisse utiliser docker avec mon compte.</simpara>
<simpara>Bon après c&#8217;est facile, un petit virtualenv pour cookiecutter, suivit d&#8217;une installation du template django.
Et puis j&#8217;ai suivi sans t <link xl:href="https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html">https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html</link></simpara>
<simpara>Alors, il télécharge les images, fait un petit update, installe les dépendances de dev, install les requirement pip &#8230;&#8203;</simpara>
<simpara>Du coup, ça prend vite de la place:
image.png</simpara>
<simpara>L&#8217;image de base python passe de 179 à 740 MB. Et là j&#8217;en ai pour presque 1,5 GB d&#8217;un coup.</simpara>
<simpara>Mais par contre, j&#8217;ai un python 3.7 direct et postgres 10 sans rien faire ou presque.</simpara>
<simpara>La partie ci-dessous a été reprise telle quelle de <link xl:href="https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html">la documentation de cookie-cutter-django</link>.</simpara>
<warning>
<simpara>le serveur de déploiement ne doit avoir qu&#8217;un accès en lecture au dépôt source.</simpara>
</warning>
<simpara>On peut aussi passer par fabric, ansible, chef ou puppet.</simpara>
</section>
</chapter>
<chapter xml:id="_autres_outils">
<title>Autres outils</title>
<simpara>Voir aussi devpi, circus, uswgi, statsd.</simpara>
<simpara>See <link xl:href="https://mattsegal.dev/nginx-django-reverse-proxy-config.html">https://mattsegal.dev/nginx-django-reverse-proxy-config.html</link></simpara>
</chapter>
<chapter xml:id="_ressources">
<title>Ressources</title>
<itemizedlist>
<listitem>
<simpara><link xl:href="https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/">https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/</link></simpara>
</listitem>
<listitem>
<simpara><link xl:href="https://docs.djangoproject.com/fr/3.0/howto/deployment/">Déploiement</link>.</simpara>
</listitem>
<listitem>
<simpara>Let&#8217;s Encrypt !</simpara>
</listitem>
</itemizedlist>
</chapter>
</part>
<part xml:id="_services_oriented_applications">
<title>Services Oriented Applications</title>
<partintro>
<simpara>Nous avons fait exprès de reprendre l&#8217;acronyme d&#8217;une <emphasis>Services Oriented Architecture</emphasis> pour cette partie.
L&#8217;objectif est de vous mettre la puce à l&#8217;oreille quant à la finalité du développement: que l&#8217;utilisateur soit humain, bot automatique ou client Web, l&#8217;objectif est de fournir des applications résilientes, disponibles et accessibles.</simpara>
<simpara>Dans cette partie, nous aborderons les vues, la mise en forme, la mise en page, la définition d&#8217;une interface REST, la définition d&#8217;une interface GraphQL et le routage d&#8217;URLs.</simpara>
</partintro>
<chapter xml:id="_application_programming_interface">
<title>Application Programming Interface</title>
<note>
<simpara><link xl:href="https://news.ycombinator.com/item?id=30221016&amp;utm_term=comment">https://news.ycombinator.com/item?id=30221016&amp;utm_term=comment</link> vs Django Rest Framework</simpara>
</note>
<note>
<simpara>Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis.</simpara>
</note>
<note>
<simpara>Voir peut-être aussi <link xl:href="https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/">https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/</link></simpara>
</note>
<simpara>Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service d&#8217;affectation.
Quelque chose comme ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered"># models.py
from django.db import models
class People(models.Model):
CIVILITY_CHOICES = (
("M", "Monsieur"),
("Mme", "Madame"),
("Dr", "Docteur"),
("Pr", "Professeur"),
("", "")
)
last_name = models.CharField(max_length=255)
first_name = models.CharField(max_length=255)
civility = models.CharField(
max_length=3,
choices=CIVILITY_CHOICES,
default=""
)
def __str__(self):
return "{}, {}".format(self.last_name, self.first_name)
class Service(models.Model):
label = models.CharField(max_length=255)
def __str__(self):
return self.label
class ContractType(models.Model):
label = models.CharField(max_length=255)
short_label = models.CharField(max_length=50)
def __str__(self):
return self.short_label
class Contract(models.Model):
people = models.ForeignKey(People, on_delete=models.CASCADE)
date_begin = models.DateField()
date_end = models.DateField(blank=True, null=True)
contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE)
service = models.ForeignKey(Service, on_delete=models.CASCADE)
def __str__(self):
if self.date_end is not None:
return "A partir du {}, jusqu'au {}, dans le service {} ({})".format(
self.date_begin,
self.date_end,
self.service,
self.contract_type
)
return "A partir du {}, à durée indéterminée, dans le service {} ({})".format(
self.date_begin,
self.service,
self.contract_type
)</programlisting>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/rest/models.png"/>
</imageobject>
<textobject><phrase>models</phrase></textobject>
</mediaobject>
</informalfigure>
</chapter>
<chapter xml:id="_configuration_2">
<title>Configuration</title>
<simpara>La configuration des points de terminaison de notre API est relativement touffue.
Il convient de:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de l&#8217;API,</simpara>
</listitem>
<listitem>
<simpara>Configurer les vues, càd le comportement de chacun des points de terminaison,</simpara>
</listitem>
<listitem>
<simpara>Configurer les points de terminaison eux-mêmes, càd les URLs permettant d&#8217;accéder aux ressources.</simpara>
</listitem>
<listitem>
<simpara>Et finalement ajouter quelques paramètres au niveau de notre application.</simpara>
</listitem>
</orderedlist>
<section xml:id="_sérialiseurs">
<title>Sérialiseurs</title>
<programlisting language="python" linenumbering="unnumbered"># serializers.py
from django.contrib.auth.models import User, Group
from rest_framework import serializers
from .models import People, Contract, Service
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")
class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class ServiceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Service
fields = ("name",)</programlisting>
</section>
<section xml:id="_vues">
<title>Vues</title>
<programlisting language="python" linenumbering="unnumbered"># views.py
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from rest_framework import permissions
from .models import People, Contract, Service
from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer
class PeopleViewSet(viewsets.ModelViewSet):
queryset = People.objects.all()
serializer_class = PeopleSerializer
permission_class = [permissions.IsAuthenticated]
class ContractViewSet(viewsets.ModelViewSet):
queryset = Contract.objects.all()
serializer_class = ContractSerializer
permission_class = [permissions.IsAuthenticated]
class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all()
serializer_class = ServiceSerializer
permission_class = [permissions.IsAuthenticated]</programlisting>
</section>
<section xml:id="_urls">
<title>URLs</title>
<programlisting language="python" linenumbering="unnumbered"># urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from core import views
router = routers.DefaultRouter()
router.register(r"people", views.PeopleViewSet)
router.register(r"contracts", views.ContractViewSet)
router.register(r"services", views.ServiceViewSet)
urlpatterns = [
path("api/v1/", include(router.urls)),
path('admin/', admin.site.urls),
]</programlisting>
</section>
<section xml:id="_paramètres">
<title>Paramètres</title>
<programlisting language="python" linenumbering="unnumbered"># settings.py
INSTALLED_APPS = [
...
"rest_framework",
...
]
...
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}</programlisting>
<simpara>A ce stade, en nous rendant sur l&#8217;URL <literal><link xl:href="http://localhost:8000/api/v1">http://localhost:8000/api/v1</link></literal>, nous obtiendrons ceci:</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/rest/api-first-example.png"/>
</imageobject>
<textobject><phrase>api first example</phrase></textobject>
</mediaobject>
</informalfigure>
</section>
</chapter>
<chapter xml:id="_modèles_et_relations">
<title>Modèles et relations</title>
<simpara>Plus haut, nous avons utilisé une relation de type <literal>HyperlinkedModelSerializer</literal>.
C&#8217;est une bonne manière pour autoriser des relations entre vos instances à partir de l&#8217;API, mais il faut reconnaître que cela reste assez limité.
Pour palier à ceci, il existe [plusieurs manières de représenter ces relations](<link xl:href="https://www.django-rest-framework.org/api-guide/relations/">https://www.django-rest-framework.org/api-guide/relations/</link>): soit <emphasis role="strong">via</emphasis> un hyperlien, comme ci-dessus, soit en utilisant les clés primaires, soit en utilisant l&#8217;URL canonique permettant d&#8217;accéder à la ressource.
La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats):</simpara>
<programlisting language="json" linenumbering="unnumbered">{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
"http://localhost:8000/api/v1/contracts/1/",
"http://localhost:8000/api/v1/contracts/2/"
]
}
]
}</programlisting>
<simpara>à ceci:</simpara>
<programlisting language="json" linenumbering="unnumbered">{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"last_name": "Bond",
"first_name": "James",
"contract_set": [
{
"date_begin": "2019-01-01",
"date_end": null,
"service": "http://localhost:8000/api/v1/services/1/"
},
{
"date_begin": "2009-01-01",
"date_end": "2021-01-01",
"service": "http://localhost:8000/api/v1/services/1/"
}
]
}
]
}</programlisting>
<simpara>La modification se limite à <emphasis role="strong">surcharger</emphasis> la propriété, pour indiquer qu&#8217;elle consiste en une instance d&#8217;un des sérialiseurs existants.
Nous passons ainsi de ceci</simpara>
<programlisting language="python" linenumbering="unnumbered">class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")</programlisting>
<simpara>à ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered">class ContractSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Contract
fields = ("date_begin", "date_end", "service")
class PeopleSerializer(serializers.HyperlinkedModelSerializer):
contract_set = ContractSerializer(many=True, read_only=True)
class Meta:
model = People
fields = ("last_name", "first_name", "contract_set")</programlisting>
<simpara>Nous ne faisons donc bien que redéfinir la propriété <literal>contract_set</literal> et indiquons qu&#8217;il s&#8217;agit à présent d&#8217;une instance de <literal>ContractSerializer</literal>, et qu&#8217;il est possible d&#8217;en avoir plusieurs.
C&#8217;est tout.</simpara>
</chapter>
<chapter xml:id="_filtres_et_recherches">
<title>Filtres et recherches</title>
<simpara>A ce stade, nous pouvons juste récupérer des informations présentes dans notre base de données, mais à part les parcourir, il est difficile d&#8217;en faire quelque chose.</simpara>
<simpara>Il est possible de jouer avec les URLs en définissant une nouvelle route ou avec les paramètres de l&#8217;URL, ce qui demanderait alors de programmer chaque cas possible - sans que le consommateur ne puisse les déduire lui-même. Une solution élégante consiste à autoriser le consommateur à filtrer les données, directement au niveau de l&#8217;API.
Ceci peut être fait. Il existe deux manières de restreindre l&#8217;ensemble des résultats retournés:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Soit au travers d&#8217;une recherche, qui permet d&#8217;effectuer une recherche textuelle, globale et par ensemble à un ensemble de champs,</simpara>
</listitem>
<listitem>
<simpara>Soit au travers d&#8217;un filtre, ce qui permet de spécifier une valeur précise à rechercher.</simpara>
</listitem>
</orderedlist>
<simpara>Dans notre exemple, la première possibilité sera utile pour rechercher une personne répondant à un ensemble de critères. Typiquement, <literal>/api/v1/people/?search=raymond bond</literal> ne nous donnera aucun résultat, alors que <literal>/api/v1/people/?search=james bond</literal> nous donnera le célèbre agent secret (qui a bien entendu un contrat chez nous&#8230;&#8203;).</simpara>
<simpara>Le second cas permettra par contre de préciser que nous souhaitons disposer de toutes les personnes dont le contrat est ultérieur à une date particulière.</simpara>
<simpara>Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de proposer immédiatement les champs, et donc d&#8217;informer le consommateur des possibilités:</simpara>
<informalfigure>
<mediaobject>
<imageobject>
<imagedata fileref="images/rest/drf-filters-and-searches.png"/>
</imageobject>
<textobject><phrase>drf filters and searches</phrase></textobject>
</mediaobject>
</informalfigure>
<section xml:id="_recherches">
<title>Recherches</title>
<simpara>La fonction de recherche est déjà implémentée au niveau de Django-Rest-Framework, et aucune dépendance supplémentaire n&#8217;est nécessaire.
Au niveau du <literal>viewset</literal>, il suffit d&#8217;ajouter deux informations:</simpara>
<programlisting language="python" linenumbering="unnumbered">...
from rest_framework import filters, viewsets
...
class PeopleViewSet(viewsets.ModelViewSet):
...
filter_backends = [filters.SearchFilter]
search_fields = ["last_name", "first_name"]
...</programlisting>
</section>
<section xml:id="_filtres">
<title>Filtres</title>
<simpara>Nous commençons par installer [le paquet <literal>django-filter</literal>](<link xl:href="https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend">https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend</link>) et nous l&#8217;ajoutons parmi les applications installées:</simpara>
<programlisting language="bash" linenumbering="unnumbered">λ pip install django-filter
Collecting django-filter
Downloading django_filter-2.4.0-py3-none-any.whl (73 kB)
|████████████████████████████████| 73 kB 2.6 MB/s
Requirement already satisfied: Django&gt;=2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from django-filter) (3.1.7)
Requirement already satisfied: asgiref&lt;4,&gt;=3.2.10 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django&gt;=2.2-&gt;django-filter) (3.3.1)
Requirement already satisfied: sqlparse&gt;=0.2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django&gt;=2.2-&gt;django-filter) (0.4.1)
Requirement already satisfied: pytz in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django&gt;=2.2-&gt;django-filter) (2021.1)
Installing collected packages: django-filter
Successfully installed django-filter-2.4.0</programlisting>
<simpara>Une fois l&#8217;installée réalisée, il reste deux choses à faire:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Ajouter <literal>django_filters</literal> parmi les applications installées:</simpara>
</listitem>
<listitem>
<simpara>Configurer la clé <literal>DEFAULT_FILTER_BACKENDS</literal> à la valeur <literal>['django_filters.rest_framework.DjangoFilterBackend']</literal>.</simpara>
</listitem>
</orderedlist>
<simpara>Vous avez suivi les étapes ci-dessus, il suffit d&#8217;adapter le fichier <literal>settings.py</literal> de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered">REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}</programlisting>
<simpara>Au niveau du viewset, il convient d&#8217;ajouter ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered">...
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
...
class PeopleViewSet(viewsets.ModelViewSet):
...
filter_backends = [DjangoFilterBackend]
filterset_fields = ('last_name',)
...</programlisting>
<simpara>A ce stade, nous avons deux problèmes:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Le champ que nous avons défini au niveau de la propriété <literal>filterset_fields</literal> exige une correspondance exacte. Ainsi, <literal>/api/v1/people/?last_name=Bon</literal> ne retourne rien, alors que <literal>/api/v1/people/?last_name=Bond</literal> nous donnera notre agent secret préféré.</simpara>
</listitem>
<listitem>
<simpara>Il n&#8217;est pas possible d&#8217;aller appliquer un critère de sélection sur la propriété d&#8217;une relation. Notre exemple proposant rechercher uniquement les relations dans le futur (ou dans le passé) tombe à l&#8217;eau.</simpara>
</listitem>
</orderedlist>
<simpara>Pour ces deux points, nous allons définir un nouveau filtre, en surchargeant une nouvelle classe dont la classe mère serait de type <literal>django_filters.FilterSet</literal>.</simpara>
<simpara>TO BE CONTINUED.</simpara>
<simpara>A noter qu&#8217;il existe un paquet [Django-Rest-Framework-filters](<link xl:href="https://github.com/philipn/django-rest-framework-filters">https://github.com/philipn/django-rest-framework-filters</link>), mais il est déprécié depuis Django 3.0, puisqu&#8217;il se base sur <literal>django.utils.six</literal> qui n&#8217;existe à présent plus. Il faut donc le faire à la main (ou patcher le paquet&#8230;&#8203;).</simpara>
</section>
</chapter>
<chapter xml:id="_urls_et_espaces_de_noms">
<title>URLs et espaces de noms</title>
<simpara>La gestion des URLs permet <emphasis role="strong">grosso modo</emphasis> d&#8217;assigner une adresse paramétrée ou non à une fonction Python. La manière simple consiste à modifier le fichier <literal>gwift/settings.py</literal> pour y ajouter nos correspondances. Par défaut, le fichier ressemble à ceci:</simpara>
<programlisting language="python" linenumbering="unnumbered"># gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]</programlisting>
<simpara>La variable <literal>urlpatterns</literal> associe un ensemble d&#8217;adresses à des fonctions.
Dans le fichier <emphasis role="strong">nu</emphasis>, seul le <emphasis role="strong">pattern</emphasis> <literal>admin</literal> est défini, et inclut toutes les adresses qui sont définies dans le
fichier <literal>admin.site.urls</literal>.</simpara>
<simpara>Django fonctionne avec des <emphasis role="strong">expressions rationnelles</emphasis> simplifiées (des <emphasis role="strong">expressions régulières</emphasis> ou <emphasis role="strong">regex</emphasis>)
pour trouver une correspondance entre une URL et la fonction qui recevra la requête et retournera une réponse.
Nous utilisons l&#8217;expression <literal>^$</literal> pour déterminer la racine de notre application, mais nous pourrions appliquer d&#8217;autres regroupements
(<literal>/home</literal>, <literal>users/&lt;profile_id&gt;</literal>, <literal>articles/&lt;year&gt;/&lt;month&gt;/&lt;day&gt;</literal>, &#8230;&#8203;).
Chaque <emphasis role="strong">variable</emphasis> déclarée dans l&#8217;expression régulière sera apparenté à un paramètre dans la fonction correspondante.
Ainsi,</simpara>
<programlisting language="python" linenumbering="unnumbered"># admin.site.urls.py</programlisting>
<simpara>Pour reprendre l&#8217;exemple où on en était resté:</simpara>
<programlisting language="python" linenumbering="unnumbered"># 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'),
]</programlisting>
<tip>
<simpara>Dans la mesure du possible, essayez toujours de <emphasis role="strong">nommer</emphasis> chaque expression.
Cela permettra notamment de les retrouver au travers de la fonction <literal>reverse</literal>, mais permettra également de simplifier vos templates.</simpara>
</tip>
<simpara>A présent, on doit tester que l&#8217;URL racine de notre application mène bien vers la fonction <literal>wish_views.wishlists</literal>.</simpara>
<simpara>Sauf que les pages <literal>about</literal> et <literal>help</literal> existent également.
Pour implémenter ce type de précédence, il faudrait implémenter les URLs de la manière suivante:</simpara>
<programlisting language="text" linenumbering="unnumbered">| about
| help
| &lt;user&gt;</programlisting>
<simpara>Mais cela signifie aussi que les utilisateurs <literal>about</literal> et <literal>help</literal> (s&#8217;ils existent&#8230;&#8203;) ne pourront jamais accéder à leur profil.
Une dernière solution serait de maintenir une liste d&#8217;authorité des noms d&#8217;utilisateur qu&#8217;il n&#8217;est pas possible d&#8217;utiliser.</simpara>
<simpara>D&#8217;où l&#8217;importance de bien définir la séquence de déinition de ces routes, ainsi que des espaces de noms.</simpara>
<simpara>Note sur les namespaces.</simpara>
<simpara>De là, découle une autre bonne pratique: l&#8217;utilisation de <emphasis>breadcrumbs</emphasis> (<link xl:href="https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template">https://stackoverflow.com/questions/826889/how-to-implement-breadcrumbs-in-a-django-template</link>) ou de guidelines de navigation.</simpara>
<section xml:id="_reverse">
<title>Reverse</title>
<simpara>En associant un nom ou un libellé à chaque URL, il est possible de récupérer sa <emphasis role="strong">traduction</emphasis>. Cela implique par contre de ne plus toucher à ce libellé par la suite&#8230;&#8203;</simpara>
<simpara>Dans le fichier <literal>urls.py</literal>, on associe le libellé <literal>wishlists</literal> à l&#8217;URL <literal>r'^$</literal> (c&#8217;est-à-dire la racine du site):</simpara>
<programlisting language="python" linenumbering="unnumbered">from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]</programlisting>
<simpara>De cette manière, dans nos templates, on peut à présent construire un lien vers la racine avec le tags suivant:</simpara>
<programlisting language="html" linenumbering="unnumbered">&lt;a href="{% url 'wishlists' %}"&gt;{{ yearvar }} Archive&lt;/a&gt;</programlisting>
<simpara>De la même manière, on peut également récupérer l&#8217;URL de destination pour n&#8217;importe quel libellé, de la manière suivante:</simpara>
<programlisting language="python" linenumbering="unnumbered">from django.core.urlresolvers import reverse_lazy
wishlists_url = reverse_lazy('wishlists')</programlisting>
</section>
</chapter>
<chapter xml:id="_i18n_l10n">
<title>i18n / l10n</title>
<simpara>La localisation (<emphasis>l10n</emphasis>) et l&#8217;internationalization (<emphasis>i18n</emphasis>) sont deux concepts proches, mais différents:</simpara>
<itemizedlist>
<listitem>
<simpara>Internationalisation: <emphasis>Preparing the software for localization. Usually done by developers.</emphasis></simpara>
</listitem>
<listitem>
<simpara>Localisation: <emphasis>Writing the translations and local formats. Usually done by translators.</emphasis></simpara>
</listitem>
</itemizedlist>
<simpara>L&#8217;internationalisation est donc le processus permettant à une application d&#8217;accepter une forme de localisation.
La seconde ne va donc pas sans la première, tandis que la première ne fait qu&#8217;autoriser la seconde.</simpara>
<section xml:id="_arborescences">
<title>Arborescences</title>
<programlisting language="python" linenumbering="unnumbered"></programlisting>
<programlisting language="python" linenumbering="unnumbered"># &lt;app&gt;/management/commands/rebuild.py
"""This command manages Closure Tables implementation
It adds new levels and cleans links between entities.
This way, it's relatively easy to fetch an entire tree with just one tiny request.
"""
from django.core.management.base import BaseCommand
from rps.structure.models import Entity, EntityTreePath
class Command(BaseCommand):
def handle(self, *args, **options):
entities = Entity.objects.all()
for entity in entities:
breadcrumb = [node for node in entity.breadcrumb()]
tree = set(EntityTreePath.objects.filter(descendant=entity))
for idx, node in enumerate(breadcrumb):
tree_path, _ = EntityTreePath.objects.get_or_create(
ancestor=node, descendant=entity, weight=idx + 1
)
if tree_path in tree:
tree.remove(tree_path)
for tree_path in tree:
tree_path.delete()</programlisting>
</section>
</chapter>
<chapter xml:id="_conclusions_3">
<title>Conclusions</title>
<simpara>De part son pattern <literal>MVT</literal>, Django ne fait pas comme les autres frameworks.</simpara>
</chapter>
</part>
<part xml:id="_go_live">
<title>Go Live !</title>
<partintro>
<simpara>Pour commencer, nous allons nous concentrer sur la création d&#8217;un site ne contenant qu&#8217;une seule application, même si en pratique le site contiendra déjà plusieurs applications fournies pas django, comme nous le verrons plus loin.</simpara>
<note>
<simpara>Don&#8217;t make me think, or why I switched from JS SPAs to Ruby On Rails <link xl:href="https://news.ycombinator.com/item?id=30206989&amp;utm_term=comment">https://news.ycombinator.com/item?id=30206989&amp;utm_term=comment</link></simpara>
</note>
<simpara>Pour prendre un exemple concret, nous allons créer un site permettant de gérer des listes de souhaits, que nous appellerons <literal>gwift</literal> (pour <literal>GiFTs and WIshlisTs</literal> :)).</simpara>
<simpara>La première chose à faire est de définir nos besoins du point de vue de l&#8217;utilisateur, c&#8217;est-à-dire ce que nous souhaitons qu&#8217;un utilisateur puisse faire avec l&#8217;application.</simpara>
<simpara>Ensuite, nous pourrons traduire ces besoins en fonctionnalités et finalement effectuer le développement.</simpara>
</partintro>
<chapter xml:id="_gwift">
<title>Gwift</title>
<figure>
<title>Gwift</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/django-project-vs-apps-gwift.png"/>
</imageobject>
<textobject><phrase>django project vs apps gwift</phrase></textobject>
</mediaobject>
</figure>
</chapter>
<chapter xml:id="_besoins_utilisateurs">
<title>Besoins utilisateurs</title>
<simpara>Nous souhaitons développer un site où un utilisateur donné peut créer une liste contenant des souhaits et où d&#8217;autres utilisateurs, authentifiés ou non, peuvent choisir les souhaits à la réalisation desquels ils souhaitent participer.</simpara>
<simpara>Il sera nécessaire de s&#8217;authentifier pour :</simpara>
<itemizedlist>
<listitem>
<simpara>Créer une liste associée à l&#8217;utilisateur en cours</simpara>
</listitem>
<listitem>
<simpara>Ajouter un nouvel élément à une liste</simpara>
</listitem>
</itemizedlist>
<simpara>Il ne sera pas nécessaire de s&#8217;authentifier pour :</simpara>
<itemizedlist>
<listitem>
<simpara>Faire une promesse d&#8217;offre pour un élément appartenant à une liste, associée à un utilisateur.</simpara>
</listitem>
</itemizedlist>
<simpara>L&#8217;utilisateur ayant créé une liste pourra envoyer un email directement depuis le site aux personnes avec qui il souhaite partager sa liste, cet email contenant un lien permettant d&#8217;accéder à cette liste.</simpara>
<simpara>A chaque souhait, on pourrait de manière facultative ajouter un prix. Dans ce cas, le souhait pourrait aussi être subdivisé en plusieurs parties, de manière à ce que plusieurs personnes puissent participer à sa réalisation.</simpara>
<simpara>Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à dupliquer le souhait en question.</simpara>
</chapter>
<chapter xml:id="_besoins_fonctionnels">
<title>Besoins fonctionnels</title>
<section xml:id="_gestion_des_utilisateurs">
<title>Gestion des utilisateurs</title>
<simpara>Pour gérer les utilisateurs, nous allons faire en sorte de surcharger ce que Django propose: par défaut, on a une la possibilité de gérer des utilisateurs (identifiés par une adresse email, un nom, un prénom, &#8230;&#8203;) mais sans plus.</simpara>
<simpara>Ce qu&#8217;on peut souhaiter, c&#8217;est que l&#8217;utilisateur puisse s&#8217;authentifier grâce à une plateforme connue (Facebook, Twitter, Google, etc.), et qu&#8217;il puisse un minimum gérer son profil.</simpara>
</section>
<section xml:id="_gestion_des_listes">
<title>Gestion des listes</title>
<section xml:id="_modèlisation">
<title>Modèlisation</title>
<simpara>Les données suivantes doivent être associées à une liste:</simpara>
<itemizedlist>
<listitem>
<simpara>un identifiant</simpara>
</listitem>
<listitem>
<simpara>un identifiant externe (un GUID, par exemple)</simpara>
</listitem>
<listitem>
<simpara>un nom</simpara>
</listitem>
<listitem>
<simpara>une description</simpara>
</listitem>
<listitem>
<simpara>le propriétaire, associé à l&#8217;utilisateur qui l&#8217;aura créée</simpara>
</listitem>
<listitem>
<simpara>une date de création</simpara>
</listitem>
<listitem>
<simpara>une date de modification</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_fonctionnalités">
<title>Fonctionnalités</title>
<itemizedlist>
<listitem>
<simpara>Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer une liste dont il est le propriétaire</simpara>
</listitem>
<listitem>
<simpara>Un utilisateur doit pouvoir associer ou retirer des souhaits à une liste dont il est le propriétaire</simpara>
</listitem>
<listitem>
<simpara>Il faut pouvoir accéder à une liste, avec un utilisateur authentifier ou non, <emphasis role="strong">via</emphasis> son identifiant externe</simpara>
</listitem>
<listitem>
<simpara>Il faut pouvoir envoyer un email avec le lien vers la liste, contenant son identifiant externe</simpara>
</listitem>
<listitem>
<simpara>L&#8217;utilisateur doit pouvoir voir toutes les listes qui lui appartiennent</simpara>
</listitem>
</itemizedlist>
</section>
</section>
<section xml:id="_gestion_des_souhaits">
<title>Gestion des souhaits</title>
<section xml:id="_modélisation_2">
<title>Modélisation</title>
<simpara>Les données suivantes peuvent être associées à un souhait:</simpara>
<itemizedlist>
<listitem>
<simpara>un identifiant</simpara>
</listitem>
<listitem>
<simpara>identifiant de la liste</simpara>
</listitem>
<listitem>
<simpara>un nom</simpara>
</listitem>
<listitem>
<simpara>une description</simpara>
</listitem>
<listitem>
<simpara>le propriétaire</simpara>
</listitem>
<listitem>
<simpara>une date de création</simpara>
</listitem>
<listitem>
<simpara>une date de modification</simpara>
</listitem>
<listitem>
<simpara>une image, afin de représenter l&#8217;objet ou l&#8217;idée</simpara>
</listitem>
<listitem>
<simpara>un nombre (1 par défaut)</simpara>
</listitem>
<listitem>
<simpara>un prix facultatif</simpara>
</listitem>
<listitem>
<simpara>un nombre de part, facultatif également, si un prix est fourni.</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_fonctionnalités_2">
<title>Fonctionnalités</title>
<itemizedlist>
<listitem>
<simpara>Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer un souhait dont il est le propriétaire.</simpara>
</listitem>
<listitem>
<simpara>On ne peut créer un souhait sans liste associée</simpara>
</listitem>
<listitem>
<simpara>Il faut pouvoir fractionner un souhait uniquement si un prix est donné.</simpara>
</listitem>
<listitem>
<simpara>Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié ou non.</simpara>
</listitem>
<listitem>
<simpara>Il faut pouvoir réaliser un souhait ou une partie seulement, avec un utilisateur authentifié ou non.</simpara>
</listitem>
<listitem>
<simpara>Un souhait en cours de réalisation et composé de différentes parts ne peut plus être modifié.</simpara>
</listitem>
<listitem>
<simpara>Un souhait en cours de réalisation ou réalisé ne peut plus être supprimé.</simpara>
</listitem>
<listitem>
<simpara>On peut modifier le nombre de fois qu&#8217;un souhait doit être réalisé dans la limite des réalisations déjà effectuées.</simpara>
</listitem>
</itemizedlist>
</section>
</section>
<section xml:id="_gestion_des_réalisations_de_souhaits">
<title>Gestion des réalisations de souhaits</title>
<section xml:id="_modélisation_3">
<title>Modélisation</title>
<simpara>Les données suivantes peuvent être associées à une réalisation de souhait:</simpara>
<itemizedlist>
<listitem>
<simpara>identifiant du souhait</simpara>
</listitem>
<listitem>
<simpara>identifiant de l&#8217;utilisateur si connu</simpara>
</listitem>
<listitem>
<simpara>identifiant de la personne si utilisateur non connu</simpara>
</listitem>
<listitem>
<simpara>un commentaire</simpara>
</listitem>
<listitem>
<simpara>une date de réalisation</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_fonctionnalités_3">
<title>Fonctionnalités</title>
<itemizedlist>
<listitem>
<simpara>L&#8217;utilisateur doit pouvoir voir si un souhait est réalisé, en partie ou non. Il doit également avoir un pourcentage de complétion sur la possibilité de réalisation de son souhait, entre 0% et 100%.</simpara>
</listitem>
<listitem>
<simpara>L&#8217;utilisateur doit pouvoir voir la ou les personnes ayant réalisé un souhait.</simpara>
</listitem>
<listitem>
<simpara>Il y a autant de réalisation que de parts de souhait réalisées ou de nombre de fois que le souhait est réalisé.</simpara>
</listitem>
</itemizedlist>
</section>
</section>
<section xml:id="_gestion_des_personnes_réalisants_les_souhaits_et_qui_ne_sont_pas_connues">
<title>Gestion des personnes réalisants les souhaits et qui ne sont pas connues</title>
<section xml:id="_modélisation_4">
<title>Modélisation</title>
<simpara>Les données suivantes peuvent être associées à une personne réalisant un souhait:</simpara>
<itemizedlist>
<listitem>
<simpara>un identifiant</simpara>
</listitem>
<listitem>
<simpara>un nom</simpara>
</listitem>
<listitem>
<simpara>une adresse email facultative</simpara>
</listitem>
</itemizedlist>
</section>
<section xml:id="_fonctionnalités_4">
<title>Fonctionnalités</title>
<sidebar>
<simpara>Modélisation</simpara>
</sidebar>
<simpara>L&#8217;ORM de Django permet de travailler uniquement avec une définition de classes, et de faire en sorte que le lien avec la base de données soit géré uniquement de manière indirecte, par Django lui-même. On peut schématiser ce comportement par une classe = une table.</simpara>
<simpara>Comme on l&#8217;a vu dans la description des fonctionnalités, on va <emphasis role="strong">grosso modo</emphasis> avoir besoin des éléments suivants:</simpara>
<itemizedlist>
<listitem>
<simpara>Des listes de souhaits</simpara>
</listitem>
<listitem>
<simpara>Des éléments qui composent ces listes</simpara>
</listitem>
<listitem>
<simpara>Des parts pouvant composer chacun de ces éléments</simpara>
</listitem>
<listitem>
<simpara>Des utilisateurs pour gérer tout ceci.</simpara>
</listitem>
</itemizedlist>
<simpara>Nous proposons dans un premier temps d&#8217;éluder la gestion des utilisateurs, et de simplement se concentrer sur les fonctionnalités principales.
Cela nous donne ceci:</simpara>
<orderedlist numeration="loweralpha">
<listitem>
<simpara>code-block:: python</simpara>
<literallayout class="monospaced"># wish/models.py</literallayout>
<literallayout class="monospaced">from django.db import models</literallayout>
<literallayout class="monospaced">class Wishlist(models.Model):
pass</literallayout>
<literallayout class="monospaced">class Item(models.Model):
pass</literallayout>
<literallayout class="monospaced">class Part(models.Model):
pass</literallayout>
</listitem>
</orderedlist>
<simpara>Les classes sont créées, mais vides. Entrons dans les détails.</simpara>
<sidebar>
<simpara>Listes de souhaits</simpara>
</sidebar>
<simpara>Comme déjà décrit précédemment, les listes de souhaits peuvent s&#8217;apparenter simplement à un objet ayant un nom et une description. Pour rappel, voici ce qui avait été défini dans les spécifications:</simpara>
<itemizedlist>
<listitem>
<simpara>un identifiant</simpara>
</listitem>
<listitem>
<simpara>un identifiant externe</simpara>
</listitem>
<listitem>
<simpara>un nom</simpara>
</listitem>
<listitem>
<simpara>une description</simpara>
</listitem>
<listitem>
<simpara>une date de création</simpara>
</listitem>
<listitem>
<simpara>une date de modification</simpara>
</listitem>
</itemizedlist>
<simpara>Notre classe <literal>Wishlist</literal> peut être définie de la manière suivante:</simpara>
<orderedlist numeration="loweralpha">
<listitem>
<simpara>code-block:: python</simpara>
<literallayout class="monospaced"># wish/models.py</literallayout>
<literallayout class="monospaced">class Wishlist(models.Model):</literallayout>
<literallayout class="monospaced">name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
external_id = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)</literallayout>
</listitem>
</orderedlist>
<simpara>Que peut-on constater?</simpara>
<itemizedlist>
<listitem>
<simpara>Que s&#8217;il n&#8217;est pas spécifié, un identifiant <literal>id</literal> sera automatiquement généré et accessible dans le modèle. Si vous souhaitez malgré tout spécifier que ce soit un champ en particulier qui devienne la clé primaire, il suffit de l&#8217;indiquer grâce à l&#8217;attribut <literal>primary_key=True</literal>.</simpara>
</listitem>
<listitem>
<simpara>Que chaque type de champs (<literal>DateTimeField</literal>, <literal>CharField</literal>, <literal>UUIDField</literal>, etc.) a ses propres paramètres d&#8217;initialisation. Il est intéressant de les apprendre ou de se référer à la documentation en cas de doute.</simpara>
</listitem>
</itemizedlist>
<simpara>Au niveau de notre modélisation:</simpara>
<itemizedlist>
<listitem>
<simpara>La propriété <literal>created_at</literal> est gérée automatiquement par Django grâce à l&#8217;attribut <literal>auto_now_add</literal>: de cette manière, lors d&#8217;un <emphasis role="strong">ajout</emphasis>, une valeur par défaut ("<emphasis role="strong">maintenant</emphasis>") sera attribuée à cette propriété.</simpara>
</listitem>
<listitem>
<simpara>La propriété <literal>updated_at</literal> est également gérée automatique, cette fois grâce à l&#8217;attribut <literal>auto_now</literal> initialisé à <literal>True</literal>: lors d&#8217;une <emphasis role="strong">mise à jour</emphasis>, la propriété se verra automatiquement assigner la valeur du moment présent. Cela ne permet évidemment pas de gérer un historique complet et ne nous dira pas <emphasis role="strong">quels champs</emphasis> ont été modifiés, mais cela nous conviendra dans un premier temps.</simpara>
</listitem>
<listitem>
<simpara>La propriété <literal>external_id</literal> est de type <literal>UUIDField</literal>. Lorsqu&#8217;une nouvelle instance sera instanciée, cette propriété prendra la valeur générée par la fonction <literal>uuid.uuid4()</literal>. <emphasis role="strong">A priori</emphasis>, chacun des types de champs possède une propriété <literal>default</literal>, qui permet d&#8217;initialiser une valeur sur une nouvelle instance.</simpara>
</listitem>
</itemizedlist>
<sidebar>
<simpara>Souhaits</simpara>
</sidebar>
<simpara>Nos souhaits ont besoin des propriétés suivantes:</simpara>
<itemizedlist>
<listitem>
<simpara>un identifiant</simpara>
</listitem>
<listitem>
<simpara>l&#8217;identifiant de la liste auquel le souhait est lié</simpara>
</listitem>
<listitem>
<simpara>un nom</simpara>
</listitem>
<listitem>
<simpara>une description</simpara>
</listitem>
<listitem>
<simpara>le propriétaire</simpara>
</listitem>
<listitem>
<simpara>une date de création</simpara>
</listitem>
<listitem>
<simpara>une date de modification</simpara>
</listitem>
<listitem>
<simpara>une image permettant de le représenter.</simpara>
</listitem>
<listitem>
<simpara>un nombre (1 par défaut)</simpara>
</listitem>
<listitem>
<simpara>un prix facultatif</simpara>
</listitem>
<listitem>
<simpara>un nombre de part facultatif, si un prix est fourni.</simpara>
</listitem>
</itemizedlist>
<simpara>Après implémentation, cela ressemble à ceci:</simpara>
<orderedlist numeration="loweralpha">
<listitem>
<simpara>code-block:: python</simpara>
<literallayout class="monospaced"># wish/models.py</literallayout>
<literallayout class="monospaced">class Wish(models.Model):</literallayout>
<literallayout class="monospaced">wishlist = models.ForeignKey(Wishlist)
name = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
picture = models.ImageField()
numbers_available = models.IntegerField(default=1)
number_of_parts = models.IntegerField(null=True)
estimated_price = models.DecimalField(max_digits=19, decimal_places=2,
null=True)</literallayout>
</listitem>
</orderedlist>
<simpara>A nouveau, que peut-on constater ?</simpara>
<itemizedlist>
<listitem>
<simpara>Les clés étrangères sont gérées directement dans la déclaration du modèle. Un champ de type `ForeignKey &lt;<link xl:href="https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ForeignKey&gt;`_">https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ForeignKey&gt;`_</link> permet de déclarer une relation 1-N entre deux classes. Dans la même veine, une relation 1-1 sera représentée par un champ de type `OneToOneField &lt;<link xl:href="https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/&gt;`">https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/&gt;`</link><emphasis>, alors qu&#8217;une relation N-N utilisera un `ManyToManyField &lt;<link xl:href="https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/&gt;`">https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/&gt;`</link></emphasis>.</simpara>
</listitem>
<listitem>
<simpara>L&#8217;attribut <literal>default</literal> permet de spécifier une valeur initiale, utilisée lors de la construction de l&#8217;instance. Cet attribut peut également être une fonction.</simpara>
</listitem>
<listitem>
<simpara>Pour rendre un champ optionnel, il suffit de lui ajouter l&#8217;attribut <literal>null=True</literal>.</simpara>
</listitem>
<listitem>
<simpara>Comme cité ci-dessus, chaque champ possède des attributs spécifiques. Le champ <literal>DecimalField</literal> possède par exemple les attributs <literal>max_digits</literal> et <literal>decimal_places</literal>, qui nous permettra de représenter une valeur comprise entre 0 et plus d&#8217;un milliard (avec deux chiffres décimaux).</simpara>
</listitem>
<listitem>
<simpara>L&#8217;ajout d&#8217;un champ de type <literal>ImageField</literal> nécessite l&#8217;installation de <literal>pillow</literal> pour la gestion des images. Nous l&#8217;ajoutons donc à nos pré-requis, dans le fichier <literal>requirements/base.txt</literal>.</simpara>
</listitem>
</itemizedlist>
<sidebar>
<simpara>Parts</simpara>
</sidebar>
<simpara>Les parts ont besoins des propriétés suivantes:</simpara>
<itemizedlist>
<listitem>
<simpara>un identifiant</simpara>
</listitem>
<listitem>
<simpara>identifiant du souhait</simpara>
</listitem>
<listitem>
<simpara>identifiant de l&#8217;utilisateur si connu</simpara>
</listitem>
<listitem>
<simpara>identifiant de la personne si utilisateur non connu</simpara>
</listitem>
<listitem>
<simpara>un commentaire</simpara>
</listitem>
<listitem>
<simpara>une date de réalisation</simpara>
</listitem>
</itemizedlist>
<simpara>Elles constituent la dernière étape de notre modélisation et représente la réalisation d&#8217;un souhait. Il y aura autant de part d&#8217;un souhait que le nombre de souhait à réaliser fois le nombre de part.</simpara>
<simpara>Elles permettent à un utilisateur de participer au souhait émis par un autre utilisateur. Pour les modéliser, une part est liée d&#8217;un côté à un souhait, et d&#8217;autre part à un utilisateur. Cela nous donne ceci:</simpara>
<orderedlist numeration="loweralpha">
<listitem>
<simpara>code-block:: python</simpara>
<literallayout class="monospaced">from django.contrib.auth.models import User</literallayout>
<literallayout class="monospaced">class WishPart(models.Model):</literallayout>
<literallayout class="monospaced">wish = models.ForeignKey(Wish)
user = models.ForeignKey(User, null=True)
unknown_user = models.ForeignKey(UnknownUser, null=True)
comment = models.TextField(null=True, blank=True)
done_at = models.DateTimeField(auto_now_add=True)</literallayout>
</listitem>
</orderedlist>
<simpara>La classe <literal>User</literal> référencée au début du snippet correspond à l&#8217;utilisateur qui sera connecté. Ceci est géré par Django. Lorsqu&#8217;une requête est effectuée et est transmise au serveur, cette information sera disponible grâce à l&#8217;objet <literal>request.user</literal>, transmis à chaque fonction ou <emphasis role="strong">Class-based-view</emphasis>. C&#8217;est un des avantages d&#8217;un framework tout intégré: il vient <emphasis role="strong">batteries-included</emphasis> et beaucoup de détails ne doivent pas être pris en compte. Pour le moment, nous nous limiterons à ceci. Par la suite, nous verrons comment améliorer la gestion des profils utilisateurs, comment y ajouter des informations et comment gérer les cas particuliers.</simpara>
<simpara>La classe <literal>UnknownUser</literal> permet de représenter un utilisateur non enregistré sur le site et est définie au point suivant.</simpara>
<sidebar>
<simpara>Utilisateurs inconnus</simpara>
</sidebar>
<orderedlist numeration="loweralpha">
<listitem>
<simpara>todo:: je supprimerais pour que tous les utilisateurs soient gérés au même endroit.</simpara>
</listitem>
</orderedlist>
<simpara>Pour chaque réalisation d&#8217;un souhait par quelqu&#8217;un, il est nécessaire de sauver les données suivantes, même si l&#8217;utilisateur n&#8217;est pas enregistré sur le site:</simpara>
<itemizedlist>
<listitem>
<simpara>un identifiant</simpara>
</listitem>
<listitem>
<simpara>un nom</simpara>
</listitem>
<listitem>
<simpara>une adresse email. Cette adresse email sera unique dans notre base de données, pour ne pas créer une nouvelle occurence si un même utilisateur participe à la réalisation de plusieurs souhaits.</simpara>
</listitem>
</itemizedlist>
<simpara>Ceci nous donne après implémentation:</simpara>
<orderedlist numeration="loweralpha">
<listitem>
<simpara>code-block:: python</simpara>
<literallayout class="monospaced">class UnkownUser(models.Model):</literallayout>
<literallayout class="monospaced">name = models.CharField(max_length=255)
email = models.CharField(email = models.CharField(max_length=255, unique=True)</literallayout>
</listitem>
</orderedlist>
</section>
</section>
</chapter>
<chapter xml:id="_tests_unitaires_2">
<title>Tests unitaires</title>
<section xml:id="_pourquoi_sennuyer_à_écrire_des_tests">
<title>Pourquoi s&#8217;ennuyer à écrire des tests?</title>
<simpara>Traduit grossièrement depuis un article sur `https://medium.com &lt;<link xl:href="https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21&gt;`_">https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21&gt;`_</link>:</simpara>
<literallayout class="monospaced">Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont</literallayout>
<literallayout class="monospaced">Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:</literallayout>
<orderedlist numeration="arabic">
<listitem>
<simpara>Aide au design: écrire des tests avant d&#8217;écrire le code vous donnera une meilleure perspective sur le design à appliquer aux API.</simpara>
</listitem>
<listitem>
<simpara>Documentation (pour les développeurs): chaque description d&#8217;un test</simpara>
</listitem>
<listitem>
<simpara>Tester votre compréhension en tant que développeur:</simpara>
</listitem>
<listitem>
<simpara>Assurance qualité: des tests,
5.</simpara>
</listitem>
</orderedlist>
</section>
<section xml:id="_why_bother_with_test_discipline">
<title>Why Bother with Test Discipline?</title>
<simpara>Your tests are your first and best line of defense against software defects. Your tests are more important than linting &amp; static analysis (which can only find a subclass of errors, not problems with your actual program logic). Tests are as important as the implementation itself (all that matters is that the code meets the requirementhow its implemented doesnt matter at all unless its implemented poorly).</simpara>
<simpara>Unit tests combine many features that make them your secret weapon to application success:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Design aid: Writing tests first gives you a clearer perspective on the ideal API design.</simpara>
</listitem>
<listitem>
<simpara>Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.</simpara>
</listitem>
<listitem>
<simpara>Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?</simpara>
</listitem>
<listitem>
<simpara>Quality Assurance: Manual QA is error prone. In my experience, its impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features.</simpara>
</listitem>
<listitem>
<simpara>Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.</simpara>
</listitem>
</orderedlist>
<simpara>Unit tests dont need to be twisted or manipulated to serve all of those broad-ranging goals. Rather, it is in the essential nature of a unit test to satisfy all of those needs. These benefits are all side-effects of a well-written test suite with good coverage.</simpara>
</section>
<section xml:id="_what_are_you_testing">
<title>What are you testing?</title>
<orderedlist numeration="arabic">
<listitem>
<simpara>What component aspect are you testing?</simpara>
</listitem>
<listitem>
<simpara>What should the feature do? What specific behavior requirement are you testing?</simpara>
</listitem>
</orderedlist>
</section>
<section xml:id="_couverture_de_code_2">
<title>Couverture de code</title>
<simpara>On a vu au chapitre 1 qu&#8217;il était possible d&#8217;obtenir une couverture de code, c&#8217;est-à-dire un pourcentage.</simpara>
</section>
<section xml:id="_comment_tester">
<title>Comment tester ?</title>
<simpara>Il y a deux manières d&#8217;écrire les tests: soit avant, soit après l&#8217;implémentation. Oui, idéalement, les tests doivent être écrits à l&#8217;avance. Entre nous, on ne va pas râler si vous faites l&#8217;inverse, l&#8217;important étant que vous le fassiez. Une bonne métrique pour vérifier l&#8217;avancement des tests est la couverture de code.</simpara>
<simpara>Pour l&#8217;exemple, nous allons écrire la fonction <literal>percentage_of_completion</literal> sur la classe <literal>Wish</literal>, et nous allons spécifier les résultats attendus avant même d&#8217;implémenter son contenu. Prenons le cas où nous écrivons la méthode avant son test:</simpara>
<programlisting language="python" linenumbering="unnumbered">class Wish(models.Model):
[...]
@property
def percentage_of_completion(self):
"""
Calcule le pourcentage de complétion pour un élément.
"""
number_of_linked_parts = WishPart.objects.filter(wish=self).count()
total = self.number_of_parts * self.numbers_available
percentage = (number_of_linked_parts / total)
return percentage * 100</programlisting>
<simpara>Lancez maintenant la couverture de code. Vous obtiendrez ceci:</simpara>
<programlisting language="text" linenumbering="unnumbered">$ coverage run --source "." src/manage.py test wish
$ coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 36 5 0 0 88%
------------------------------------------------------------------
TOTAL 69 5 4 1 93%</programlisting>
<simpara>Si vous générez le rapport HTML avec la commande <literal>coverage html</literal> et que vous ouvrez le fichier <literal>coverage_html_report/src_wish_models_py.html</literal>, vous verrez que les méthodes en rouge ne sont pas testées.
<emphasis role="strong">A contrario</emphasis>, la couverture de code atteignait <emphasis role="strong">98%</emphasis> avant l&#8217;ajout de cette nouvelle méthode.</simpara>
<simpara>Pour cela, on va utiliser un fichier <literal>tests.py</literal> dans notre application <literal>wish</literal>. <emphasis role="strong">A priori</emphasis>, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application.</simpara>
<programlisting language="python" linenumbering="unnumbered">from django.test import TestCase
class TestWishModel(TestCase):
def test_percentage_of_completion(self):
"""
Vérifie que le pourcentage de complétion d'un souhait
est correctement calculé.
Sur base d'un souhait, on crée quatre parts et on vérifie
que les valeurs s'étalent correctement sur 25%, 50%, 75% et 100%.
"""
wishlist = Wishlist(name='Fake WishList',
description='This is a faked wishlist')
wishlist.save()
wish = Wish(wishlist=wishlist,
name='Fake Wish',
description='This is a faked wish',
number_of_parts=4)
wish.save()
part1 = WishPart(wish=wish, comment='part1')
part1.save()
self.assertEqual(25, wish.percentage_of_completion)
part2 = WishPart(wish=wish, comment='part2')
part2.save()
self.assertEqual(50, wish.percentage_of_completion)
part3 = WishPart(wish=wish, comment='part3')
part3.save()
self.assertEqual(75, wish.percentage_of_completion)
part4 = WishPart(wish=wish, comment='part4')
part4.save()
self.assertEqual(100, wish.percentage_of_completion)</programlisting>
<simpara>L&#8217;attribut <literal>@property</literal> sur la méthode <literal>percentage_of_completion()</literal> va nous permettre d&#8217;appeler directement la méthode <literal>percentage_of_completion()</literal> comme s&#8217;il s&#8217;agissait d&#8217;une propriété de la classe, au même titre que les champs <literal>number_of_parts</literal> ou <literal>numbers_available</literal>. Attention que ce type de méthode contactera la base de données à chaque fois qu&#8217;elle sera appelée. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de comportement a très peu d&#8217;impacts, mais ce n&#8217;est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de <emphasis role="strong">cache</emphasis>, que nous aborderons plus loin.</simpara>
<simpara>En relançant la couverture de code, on voit à présent que nous arrivons à 99%:</simpara>
<programlisting language="shell" linenumbering="unnumbered">$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 34 0 0 0 100%
src\wish\tests.py 20 0 0 0 100%
------------------------------------------------------------------
TOTAL 87 0 4 1 99%</programlisting>
<simpara>En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code.</simpara>
</section>
<section xml:id="_quelques_liens_utiles">
<title>Quelques liens utiles</title>
<itemizedlist>
<listitem>
<simpara>`Django factory boy &lt;<link xl:href="https://github.com/rbarrois/django-factory_boy/tree/v1.0.0&gt;`_">https://github.com/rbarrois/django-factory_boy/tree/v1.0.0&gt;`_</link></simpara>
</listitem>
</itemizedlist>
</section>
</chapter>
<chapter xml:id="_refactoring">
<title>Refactoring</title>
<simpara>On constate que plusieurs classes possèdent les mêmes propriétés <literal>created_at</literal> et <literal>updated_at</literal>, initialisées aux mêmes valeurs. Pour gagner en cohérence, nous allons créer une classe dans laquelle nous définirons ces deux champs, et nous ferons en sorte que les classes <literal>Wishlist</literal>, <literal>Item</literal> et <literal>Part</literal> en héritent. Django gère trois sortes d&#8217;héritage:</simpara>
<itemizedlist>
<listitem>
<simpara>L&#8217;héritage par classe abstraite</simpara>
</listitem>
<listitem>
<simpara>L&#8217;héritage classique</simpara>
</listitem>
<listitem>
<simpara>L&#8217;héritage par classe proxy.</simpara>
</listitem>
</itemizedlist>
<section xml:id="_classe_abstraite">
<title>Classe abstraite</title>
<simpara>L&#8217;héritage par classe abstraite consiste à déterminer une classe mère qui ne sera jamais instanciée. C&#8217;est utile pour définir des champs qui se répèteront dans plusieurs autres classes et surtout pour respecter le principe de DRY. Comme la classe mère ne sera jamais instanciée, ces champs seront en fait dupliqués physiquement, et traduits en SQL, dans chacune des classes filles.</simpara>
<programlisting language="python" linenumbering="unnumbered"># wish/models.py
class AbstractModel(models.Model):
class Meta:
abstract = True
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Wishlist(AbstractModel):
pass
class Item(AbstractModel):
pass
class Part(AbstractModel):
pass</programlisting>
<simpara>En traduisant ceci en SQL, on aura en fait trois tables, chacune reprenant les champs <literal>created_at</literal> et <literal>updated_at</literal>, ainsi que son propre identifiant:</simpara>
<programlisting language="sql" linenumbering="unnumbered">--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_wishlist" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_item" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_part" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
COMMIT;</programlisting>
</section>
<section xml:id="_héritage_classique">
<title>Héritage classique</title>
<simpara>L&#8217;héritage classique est généralement déconseillé, car il peut introduire très rapidement un problème de performances: en reprenant l&#8217;exemple introduit avec l&#8217;héritage par classe abstraite, et en omettant l&#8217;attribut <literal>abstract = True</literal>, on se retrouvera en fait avec quatre tables SQL:</simpara>
<itemizedlist>
<listitem>
<simpara>Une table <literal>AbstractModel</literal>, qui reprend les deux champs <literal>created_at</literal> et <literal>updated_at</literal></simpara>
</listitem>
<listitem>
<simpara>Une table <literal>Wishlist</literal></simpara>
</listitem>
<listitem>
<simpara>Une table <literal>Item</literal></simpara>
</listitem>
<listitem>
<simpara>Une table <literal>Part</literal>.</simpara>
</listitem>
</itemizedlist>
<simpara>A nouveau, en analysant la sortie SQL de cette modélisation, on obtient ceci:</simpara>
<programlisting language="sql" linenumbering="unnumbered">--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_abstractmodel" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_wishlist" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_item" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_part" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
COMMIT;</programlisting>
<simpara>Le problème est que les identifiants seront définis et incrémentés au niveau de la table mère. Pour obtenir les informations héritées, nous seront obligés de faire une jointure. En gros, impossible d&#8217;obtenir les données complètes pour l&#8217;une des classes de notre travail de base sans effectuer un <emphasis role="strong">join</emphasis> sur la classe mère.</simpara>
<simpara>Dans ce sens, cela va encore&#8230;&#8203; Mais imaginez que vous définissiez une classe <literal>Wishlist</literal>, de laquelle héritent les classes <literal>ChristmasWishlist</literal> et <literal>EasterWishlist</literal>: pour obtenir la liste complètes des listes de souhaits, il vous faudra faire une jointure <emphasis role="strong">externe</emphasis> sur chacune des tables possibles, avant même d&#8217;avoir commencé à remplir vos données. Il est parfois nécessaire de passer par cette modélisation, mais en étant conscient des risques inhérents.</simpara>
</section>
<section xml:id="_classe_proxy">
<title>Classe proxy</title>
<simpara>Lorsqu&#8217;on définit une classe de type <emphasis role="strong">proxy</emphasis>, on fait en sorte que cette nouvelle classe ne définisse aucun nouveau champ sur la classe mère. Cela ne change dès lors rien à la traduction du modèle de données en SQL, puisque la classe mère sera traduite par une table, et la classe fille ira récupérer les mêmes informations dans la même table: elle ne fera qu&#8217;ajouter ou modifier un comportement dynamiquement, sans ajouter d&#8217;emplacements de stockage supplémentaires.</simpara>
<simpara>Nous pourrions ainsi définir les classes suivantes:</simpara>
<programlisting language="python" linenumbering="unnumbered"># wish/models.py
class Wishlist(models.Model):
name = models.CharField(max_length=255)
description = models.CharField(max_length=2000)
expiration_date = models.DateField()
@staticmethod
def create(self, name, description, expiration_date=None):
wishlist = Wishlist()
wishlist.name = name
wishlist.description = description
wishlist.expiration_date = expiration_date
wishlist.save()
return wishlist
class ChristmasWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
christmas = datetime(current_year, 12, 31)
w = Wishlist.create(name, description, christmas)
w.save()
class EasterWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
expiration_date = datetime(current_year, 4, 1)
w = Wishlist.create(name, description, expiration_date)
w.save()</programlisting>
<sidebar>
<simpara>Gestion des utilisateurs</simpara>
</sidebar>
<simpara>Dans les spécifications, nous souhaitions pouvoir associer un utilisateur à une liste (<emphasis role="strong">le propriétaire</emphasis>) et un utilisateur à une part (<emphasis role="strong">le donateur</emphasis>). Par défaut, Django offre une gestion simplifiée des utilisateurs (pas de connexion LDAP, pas de double authentification, &#8230;&#8203;): juste un utilisateur et un mot de passe. Pour y accéder, un paramètre par défaut est défini dans votre fichier de settings: <literal>AUTH_USER_MODEL</literal>.</simpara>
</section>
</chapter>
<chapter xml:id="_khana">
<title>Khana</title>
<simpara>Khana est une application de suivi d&#8217;apprentissage pour des élèves ou étudiants.
Nous voulons pouvoir:</simpara>
<orderedlist numeration="arabic">
<listitem>
<simpara>Lister les élèves</simpara>
</listitem>
<listitem>
<simpara>Faire des listes de présence pour les élèves</simpara>
</listitem>
<listitem>
<simpara>Pouvoir planifier ses cours</simpara>
</listitem>
<listitem>
<simpara>Pouvoir suivre l&#8217;apprentissage des élèves, les liens qu&#8217;ils ont entre les éléments à apprendre:</simpara>
</listitem>
<listitem>
<simpara>pour écrire une phrase, il faut pouvoir écrire des mots, connaître la grammaire, et connaître la conjugaison</simpara>
</listitem>
<listitem>
<simpara>pour écrire des mots, il faut savoir écrire des lettres</simpara>
</listitem>
<listitem>
<simpara>&#8230;&#8203;</simpara>
</listitem>
</orderedlist>
<simpara>Plusieurs professeurs s&#8217;occupent d&#8217;une même classe; il faut pouvoir écrire des notes, envoyer des messages aux autres professeurs, etc.</simpara>
<simpara>Il faut également pouvoir définir des dates de contrôle, voir combien de semaines il reste pour s&#8217;assurer d&#8217;avoir vu toute la matiètre.</simpara>
<simpara>Et pouvoir encoder les points des contrôles.</simpara>
<figure>
<title>Khana</title>
<mediaobject>
<imageobject>
<imagedata fileref="images/django/django-project-vs-apps-khana.png"/>
</imageobject>
<textobject><phrase>django project vs apps khana</phrase></textobject>
</mediaobject>
</figure>
<simpara>Unresolved directive in part-5-go-live/_index.adoc - include::legacy/_main.adoc[]</simpara>
</chapter>
</part>
<part xml:id="_ressources_et_bibliographie">
<title>Ressources et bibliographie</title>
</part>
<glossary xml:id="_glossaire">
<title>Glossaire</title>
<variablelist>
<varlistentry>
<term>http</term>
<listitem>
<simpara><emphasis>HyperText Transfer Protocol</emphasis>, ou plus généralement le protocole utilisé (et détourné) pour tout ce qui touche au <emphasis role="strong">World Wide Web</emphasis>. Il existe beaucoup d&#8217;autres protocoles d&#8217;échange de données, comme <link xl:href="https://fr.wikipedia.org/wiki/Gopher">Gopher</link>, <link xl:href="https://fr.wikipedia.org/wiki/File_Transfer_Protocol">FTP</link> ou <link xl:href="https://fr.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol">SMTP</link>.</simpara>
</listitem>
</varlistentry>
<varlistentry>
<term>IaaS</term>
<listitem>
<simpara><emphasis>Infrastructure as a Service</emphasis>, où un tiers vous fournit des machines (généralement virtuelles) que vous devrez ensuite gérer en bon père de famille. L&#8217;IaaS propose souvent une API, qui vous permet d&#8217;intégrer la durée de vie de chaque machine dans vos flux - en créant, augmentant, détruisant une machine lorsque cela s&#8217;avère nécessaire.</simpara>
</listitem>
</varlistentry>
<varlistentry>
<term>MVC</term>
<listitem>
<simpara>Le modèle <emphasis>Model-View-Controler</emphasis> est un patron de conception autorisant un faible couplage entre la gestion des données (le <emphasis>Modèle</emphasis>), l&#8217;affichage et le traitement de celles (la <emphasis>Vue</emphasis>) et la glue entre ces deux composants (au travers du <emphasis>Contrôleur</emphasis>). <link xl:href="https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller">Wikipédia</link></simpara>
</listitem>
</varlistentry>
<varlistentry>
<term>ORM</term>
<listitem>
<simpara><emphasis>Object Relational Mapper</emphasis>, où une instance est directement (ou à proximité) liée à un mode de persistance de données.</simpara>
</listitem>
</varlistentry>
<varlistentry>
<term>PaaS</term>
<listitem>
<simpara><emphasis>Platform as a Service</emphasis>, qui consiste à proposer les composants d&#8217;une plateforme (Redis, PostgreSQL, &#8230;&#8203;) en libre service et disponibles à la demande (quoiqu&#8217;après avoir communiqué son numéro de carte de crédit&#8230;&#8203;).</simpara>
</listitem>
</varlistentry>
<varlistentry>
<term>POO</term>
<listitem>
<simpara>La <emphasis>Programmation Orientée Objet</emphasis> est un paradigme de programmation informatique. Elle consiste en la définition et l&#8217;interaction de briques logicielles appelées objets ; un objet représente un concept, une idée ou toute entité du monde physique, comme une voiture, une personne ou encore une page d&#8217;un livre. Il possède une structure interne et un comportement, et il sait interagir avec ses pairs. Il s&#8217;agit donc de représenter ces objets et leurs relations ; l&#8217;interaction entre les objets via leurs relations permet de concevoir et réaliser les fonctionnalités attendues, de mieux résoudre le ou les problèmes. Dès lors, l&#8217;étape de modélisation revêt une importance majeure et nécessaire pour la POO. C&#8217;est elle qui permet de transcrire les éléments du réel sous forme virtuelle. <link xl:href="https://fr.wikipedia.org/wiki/Programmation_orient%C3%A9e_objet">Wikipédia</link></simpara>
</listitem>
</varlistentry>
<varlistentry>
<term>S3</term>
<listitem>
<simpara>Amazon <emphasis>Simple Storage Service</emphasis> consiste en un système d&#8217;hébergement de fichiers, quels qu&#8217;ils soient.
Il peut s&#8217;agir de fichiers de logs, de données applications, de fichiers média envoyés par vos utilisateurs, de vidéos et images ou de données de sauvegardes.</simpara>
</listitem>
</varlistentry>
</variablelist>
<formalpara>
<title><link xl:href="https://aws.amazon.com/fr/s3/">https://aws.amazon.com/fr/s3/</link></title>
<para><inlinemediaobject>
<imageobject>
<imagedata fileref="images/amazon-s3-arch.png"/>
</imageobject>
<textobject><phrase>amazon s3 arch</phrase></textobject>
</inlinemediaobject></para>
</formalpara>
</glossary>
<index xml:id="_index">
<title>Index</title>
</index>
<chapter xml:id="_bibliographie">
<title>Bibliographie</title>
<simpara>bibliography::[]</simpara>
</chapter>
</book>