gwift-book/source/main.html

4646 lines
209 KiB
HTML
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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 1.5.8">
<meta name="author" content="Cédric Declerfayt, Fred Pauchet">
<title>Minor swing with Django</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
<style>
/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
/* Uncomment @import statement below to use as custom stylesheet */
/*@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700";*/
article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}
audio,canvas,video{display:inline-block}
audio:not([controls]){display:none;height:0}
script{display:none!important}
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
a{background:transparent}
a:focus{outline:thin dotted}
a:active,a:hover{outline:0}
h1{font-size:2em;margin:.67em 0}
abbr[title]{border-bottom:1px dotted}
b,strong{font-weight:bold}
dfn{font-style:italic}
hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}
mark{background:#ff0;color:#000}
code,kbd,pre,samp{font-family:monospace;font-size:1em}
pre{white-space:pre-wrap}
q{quotes:"\201C" "\201D" "\2018" "\2019"}
small{font-size:80%}
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sup{top:-.5em}
sub{bottom:-.25em}
img{border:0}
svg:not(:root){overflow:hidden}
figure{margin:0}
fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
legend{border:0;padding:0}
button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
button,input{line-height:normal}
button,select{text-transform:none}
button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}
button[disabled],html input[disabled]{cursor:default}
input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
textarea{overflow:auto;vertical-align:top}
table{border-collapse:collapse;border-spacing:0}
*,*::before,*::after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}
html,body{font-size:100%}
body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto;tab-size:4;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
a:hover{cursor:pointer}
img,object,embed{max-width:100%;height:auto}
object,embed{height:100%}
img{-ms-interpolation-mode:bicubic}
.left{float:left!important}
.right{float:right!important}
.text-left{text-align:left!important}
.text-right{text-align:right!important}
.text-center{text-align:center!important}
.text-justify{text-align:justify!important}
.hide{display:none}
img,object,svg{display:inline-block;vertical-align:middle}
textarea{height:auto;min-height:50px}
select{width:100%}
.center{margin-left:auto;margin-right:auto}
.stretch{width:100%}
.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr}
a{color:#2156a5;text-decoration:underline;line-height:inherit}
a:hover,a:focus{color:#1d4b8f}
a img{border:none}
p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
p aside{font-size:.875em;line-height:1.35;font-style:italic}
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
h1{font-size:2.125em}
h2{font-size:1.6875em}
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
h4,h5{font-size:1.125em}
h6{font-size:1em}
hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0}
em,i{font-style:italic;line-height:inherit}
strong,b{font-weight:bold;line-height:inherit}
small{font-size:60%;line-height:inherit}
code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
ul,ol{margin-left:1.5em}
ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em}
ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit}
ul.square{list-style-type:square}
ul.circle{list-style-type:circle}
ul.disc{list-style-type:disc}
ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
dl dt{margin-bottom:.3125em;font-weight:bold}
dl dd{margin-bottom:1.25em}
abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help}
abbr{text-transform:none}
blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)}
blockquote cite::before{content:"\2014 \0020"}
blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)}
blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
h1{font-size:2.75em}
h2{font-size:2.3125em}
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
h4{font-size:1.4375em}}
table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede}
table thead,table tfoot{background:#f7f8f7}
table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7}
table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6}
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
.clearfix::after,.float-group::after{clear:both}
*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed;word-wrap:break-word}
*:not(pre)>code.nobreak{word-wrap:normal}
*:not(pre)>code.nowrap{white-space:nowrap}
pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed}
em em{font-style:normal}
strong strong{font-weight:400}
.keyseq{color:rgba(51,51,51,.8)}
kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
.keyseq kbd:first-child{margin-left:0}
.keyseq kbd:last-child{margin-right:0}
.menuseq,.menuref{color:#000}
.menuseq b:not(.caret),.menuref{font-weight:inherit}
.menuseq{word-spacing:-.02em}
.menuseq b.caret{font-size:1.25em;line-height:.8}
.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
b.button::before{content:"[";padding:0 3px 0 2px}
b.button::after{content:"]";padding:0 2px 0 3px}
p a>code:hover{color:rgba(0,0,0,.9)}
#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
#content{margin-top:1.25em}
#content::before{content:none}
#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap}
#header .details span:first-child{margin-left:-.125em}
#header .details span.email a{color:rgba(0,0,0,.85)}
#header .details br{display:none}
#header .details br+span::before{content:"\00a0\2013\00a0"}
#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
#header .details br+span#revremark::before{content:"\00a0|\00a0"}
#header #revnumber{text-transform:capitalize}
#header #revnumber::after{content:"\00a0"}
#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
#toc>ul{margin-left:.125em}
#toc ul.sectlevel0>li>a{font-style:italic}
#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
#toc li{line-height:1.3334;margin-top:.3334em}
#toc a{text-decoration:none}
#toc a:active{text-decoration:underline}
#toctitle{color:#7a2518;font-size:1.2em}
@media screen and (min-width:768px){#toctitle{font-size:1.375em}
body.toc2{padding-left:15em;padding-right:0}
#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
#toc.toc2>ul{font-size:.9em;margin-bottom:0}
#toc.toc2 ul ul{margin-left:0;padding-left:1em}
#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
body.toc2.toc-right{padding-left:0;padding-right:15em}
body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
#toc.toc2{width:20em}
#toc.toc2 #toctitle{font-size:1.375em}
#toc.toc2>ul{font-size:.95em}
#toc.toc2 ul ul{padding-left:1.25em}
body.toc2.toc-right{padding-left:0;padding-right:20em}}
#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
#content #toc>:first-child{margin-top:0}
#content #toc>:last-child{margin-bottom:0}
#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em}
#footer-text{color:rgba(255,255,255,.8);line-height:1.44}
#content{margin-bottom:.625em}
.sect1{padding-bottom:.625em}
@media screen and (min-width:768px){#content{margin-bottom:1.25em}
.sect1{padding-bottom:1.25em}}
.sect1:last-child{padding-bottom:0}
.sect1+.sect1{border-top:1px solid #e7e7e9}
#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
.paragraph.lead>p,#preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
table.tableblock #preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:inherit}
.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
.admonitionblock>table td.icon{text-align:center;width:80px}
.admonitionblock>table td.icon img{max-width:none}
.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6)}
.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px}
.exampleblock>.content>:first-child{margin-top:0}
.exampleblock>.content>:last-child{margin-bottom:0}
.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
.sidebarblock>:first-child{margin-top:0}
.sidebarblock>:last-child{margin-bottom:0}
.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8}
.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1}
.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;overflow-x:auto;padding:1em;font-size:.8125em}
@media screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}
@media screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}
.literalblock pre.nowrap,.literalblock pre.nowrap pre,.listingblock pre.nowrap,.listingblock pre.nowrap pre{white-space:pre;word-wrap:normal}
.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)}
.listingblock pre.highlightjs{padding:0}
.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px}
.listingblock pre.prettyprint{border-width:0}
.listingblock>.content{position:relative}
.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999}
.listingblock:hover code[data-lang]::before{display:block}
.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:#999}
.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none}
table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45}
table.pyhltable td.code{padding-left:.75em;padding-right:0}
pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #dddddf}
pre.pygments .lineno{display:inline-block;margin-right:.25em}
table.pyhltable .linenodiv{background:none!important;padding-right:0!important}
.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em}
.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
.quoteblock blockquote{margin:0;padding:0;border:0}
.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
.verseblock{margin:0 1em 1.25em}
.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
.verseblock pre strong{font-weight:400}
.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
.quoteblock .attribution br,.verseblock .attribution br{display:none}
.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
.quoteblock.abstract{margin:0 1em 1.25em;display:block}
.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
.quoteblock.excerpt,.quoteblock .quoteblock{margin:0 0 1.25em;padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;text-align:left;margin-right:0}
table.tableblock{max-width:100%;border-collapse:separate}
p.tableblock:last-child{margin-bottom:0}
td.tableblock>.content{margin-bottom:-1.25em}
table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
table.grid-all>thead>tr>.tableblock,table.grid-all>tbody>tr>.tableblock{border-width:0 1px 1px 0}
table.grid-all>tfoot>tr>.tableblock{border-width:1px 1px 0 0}
table.grid-cols>*>tr>.tableblock{border-width:0 1px 0 0}
table.grid-rows>thead>tr>.tableblock,table.grid-rows>tbody>tr>.tableblock{border-width:0 0 1px}
table.grid-rows>tfoot>tr>.tableblock{border-width:1px 0 0}
table.grid-all>*>tr>.tableblock:last-child,table.grid-cols>*>tr>.tableblock:last-child{border-right-width:0}
table.grid-all>tbody>tr:last-child>.tableblock,table.grid-all>thead:last-child>tr>.tableblock,table.grid-rows>tbody>tr:last-child>.tableblock,table.grid-rows>thead:last-child>tr>.tableblock{border-bottom-width:0}
table.frame-all{border-width:1px}
table.frame-sides{border-width:0 1px}
table.frame-topbot,table.frame-ends{border-width:1px 0}
table.stripes-all tr,table.stripes-odd tr:nth-of-type(odd){background:#f8f8f7}
table.stripes-none tr,table.stripes-odd tr:nth-of-type(even){background:none}
th.halign-left,td.halign-left{text-align:left}
th.halign-right,td.halign-right{text-align:right}
th.halign-center,td.halign-center{text-align:center}
th.valign-top,td.valign-top{vertical-align:top}
th.valign-bottom,td.valign-bottom{vertical-align:bottom}
th.valign-middle,td.valign-middle{vertical-align:middle}
table thead th,table tfoot th{font-weight:bold}
tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7}
tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
p.tableblock>code:only-child{background:none;padding:0}
p.tableblock{font-size:1em}
td>div.verse{white-space:pre}
ol{margin-left:1.75em}
ul li ol{margin-left:1.5em}
dl dd{margin-left:1.125em}
dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
ul.unstyled,ol.unstyled{margin-left:0}
ul.checklist{margin-left:.625em}
ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
ul.checklist li>p:first-child>input[type="checkbox"]:first-child{margin-right:.25em}
ul.inline{display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
ul.inline>li{margin-left:1.25em}
.unstyled dl dt{font-weight:400;font-style:normal}
ol.arabic{list-style-type:decimal}
ol.decimal{list-style-type:decimal-leading-zero}
ol.loweralpha{list-style-type:lower-alpha}
ol.upperalpha{list-style-type:upper-alpha}
ol.lowerroman{list-style-type:lower-roman}
ol.upperroman{list-style-type:upper-roman}
ol.lowergreek{list-style-type:lower-greek}
.hdlist>table,.colist>table{border:0;background:none}
.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
td.hdlist1{font-weight:bold;padding-bottom:1.25em}
.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
.colist td:not([class]):first-child img{max-width:none}
.colist td:not([class]):last-child{padding:.25em 0}
.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd}
.imageblock.left{margin:.25em .625em 1.25em 0}
.imageblock.right{margin:.25em 0 1.25em .625em}
.imageblock>.title{margin-bottom:0}
.imageblock.thumb,.imageblock.th{border-width:6px}
.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
.image.left{margin-right:.625em}
.image.right{margin-left:.625em}
a.image{text-decoration:none;display:inline-block}
a.image object{pointer-events:none}
sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
sup.footnote a,sup.footnoteref a{text-decoration:none}
sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
#footnotes .footnote:last-of-type{margin-bottom:0}
#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0}
.gist .file-data>table td.line-data{width:99%}
div.unbreakable{page-break-inside:avoid}
.big{font-size:larger}
.small{font-size:smaller}
.underline{text-decoration:underline}
.overline{text-decoration:overline}
.line-through{text-decoration:line-through}
.aqua{color:#00bfbf}
.aqua-background{background-color:#00fafa}
.black{color:#000}
.black-background{background-color:#000}
.blue{color:#0000bf}
.blue-background{background-color:#0000fa}
.fuchsia{color:#bf00bf}
.fuchsia-background{background-color:#fa00fa}
.gray{color:#606060}
.gray-background{background-color:#7d7d7d}
.green{color:#006000}
.green-background{background-color:#007d00}
.lime{color:#00bf00}
.lime-background{background-color:#00fa00}
.maroon{color:#600000}
.maroon-background{background-color:#7d0000}
.navy{color:#000060}
.navy-background{background-color:#00007d}
.olive{color:#606000}
.olive-background{background-color:#7d7d00}
.purple{color:#600060}
.purple-background{background-color:#7d007d}
.red{color:#bf0000}
.red-background{background-color:#fa0000}
.silver{color:#909090}
.silver-background{background-color:#bcbcbc}
.teal{color:#006060}
.teal-background{background-color:#007d7d}
.white{color:#bfbfbf}
.white-background{background-color:#fafafa}
.yellow{color:#bfbf00}
.yellow-background{background-color:#fafa00}
span.icon>.fa{cursor:default}
a span.icon>.fa{cursor:inherit}
.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
.conum[data-value] *{color:#fff!important}
.conum[data-value]+b{display:none}
.conum[data-value]::after{content:attr(data-value)}
pre .conum[data-value]{position:relative;top:-.125em}
b.conum *{color:inherit!important}
.conum:not([data-value]):empty{display:none}
dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
h1,h2,p,td.content,span.alt{letter-spacing:-.01em}
p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
p,blockquote,dt,td.content,span.alt{font-size:1.0625rem}
p{margin-bottom:1.25rem}
.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc}
.print-only{display:none!important}
@page{margin:1.25cm .75cm}
@media print{*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}
html{font-size:80%}
a{color:inherit!important;text-decoration:underline!important}
a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
abbr[title]::after{content:" (" attr(title) ")"}
pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
thead{display:table-header-group}
svg{max-width:100%}
p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
#toc,.sidebarblock,.exampleblock>.content{background:none!important}
#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
body.book #header{text-align:center}
body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
body.book #header .details{border:0!important;display:block;padding:0!important}
body.book #header .details span:first-child{margin-left:0!important}
body.book #header .details br{display:block}
body.book #header .details br+span::before{content:none!important}
body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
.listingblock code[data-lang]::before{display:block}
#footer{padding:0 .9375em}
.hide-on-print{display:none!important}
.print-only{display:block!important}
.hide-for-print{display:none!important}
.show-for-print{display:inherit!important}}
@media print,amzn-kf8{#header>h1:first-child{margin-top:1.25rem}
.sect1{padding:0!important}
.sect1+.sect1{border:0}
#footer{background:none}
#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
</style>
</head>
<body class="book">
<div id="header">
<h1>Minor swing with Django</h1>
<div class="details">
<span id="author" class="author">Cédric Declerfayt</span><br>
<span id="email" class="email"><a href="mailto:jaguarondi27@gmail.com">jaguarondi27@gmail.com</a></span><br>
<span id="author2" class="author">Fred Pauchet</span><br>
<span id="email2" class="email"><a href="mailto:fred@grimbox.be">fred@grimbox.be</a></span><br>
</div>
<div id="toc" class="toc">
<div id="toctitle">Table of Contents</div>
<ul class="sectlevel0">
<li><a href="#_environnement_de_travail">Environnement de travail</a>
<ul class="sectlevel1">
<li><a href="#_méthodologie_de_travail">1. Méthodologie de travail</a></li>
<li><a href="#_environnements_virtuels">2. Environnements virtuels</a>
<ul class="sectlevel2">
<li><a href="#_création_de_lenvironnement_virtuel">2.1. Création de l&#8217;environnement virtuel</a></li>
<li><a href="#_installation_de_django_et_création_du_répertoire_de_travail">2.2. Installation de Django et création du répertoire de travail</a></li>
<li><a href="#_gestion_des_dépendances">2.3. Gestion des dépendances</a></li>
<li><a href="#_structure_finale_de_lenvironnement">2.4. Structure finale de l&#8217;environnement</a></li>
</ul>
</li>
<li><a href="#_django">3. Django</a>
<ul class="sectlevel2">
<li><a href="#_gestion">3.1. Gestion</a></li>
<li><a href="#_structure_dune_application">3.2. Structure d&#8217;une application</a></li>
<li><a href="#_tests_unitaires">3.3. Tests unitaires</a></li>
</ul>
</li>
<li><a href="#_construire_des_applications_maintenables">4. Construire des applications maintenables</a>
<ul class="sectlevel2">
<li><a href="#_au_niveau_des_méthodes_et_fonctions">4.1. Au niveau des méthodes et fonctions</a></li>
<li><a href="#_au_niveau_des_classes">4.2. Au niveau des classes</a></li>
<li><a href="#_au_niveau_des_composants">4.3. Au niveau des composants</a></li>
<li><a href="#_de_manière_plus_générale">4.4. De manière plus générale</a></li>
<li><a href="#_en_pratique">4.5. En pratique</a></li>
</ul>
</li>
<li><a href="#_chaîne_doutils">5. Chaîne d&#8217;outils</a>
<ul class="sectlevel2">
<li><a href="#_pep8">5.1. PEP8</a></li>
<li><a href="#_pep8_flake8_pylint">5.2. pep8, flake8, pylint</a></li>
<li><a href="#_black">5.3. Black</a></li>
<li><a href="#_pytest">5.4. pytest</a></li>
<li><a href="#_mypy">5.5. mypy</a></li>
</ul>
</li>
<li><a href="#_git">6. Git</a></li>
<li><a href="#_graphviz">7. graphviz</a></li>
<li><a href="#_en_résumé">8. En résumé</a></li>
</ul>
</li>
<li><a href="#_déploiement">Déploiement</a>
<ul class="sectlevel1">
<li><a href="#_un_peu_de_théorie_les_principales_étapes">9. Un peu de théorie&#8230;&#8203; Les principales étapes</a>
<ul class="sectlevel2">
<li><a href="#_définition_de_linfrastructure">9.1. Définition de l&#8217;infrastructure</a></li>
<li><a href="#_configuration_et_sécurisation_de_la_machine_hôte">9.2. Configuration et sécurisation de la machine hôte</a></li>
<li><a href="#_mise_à_jour">9.3. Mise à jour</a></li>
<li><a href="#_supervision">9.4. Supervision</a></li>
</ul>
</li>
<li><a href="#_déploiement_sur_centos">10. Déploiement sur CentOS</a>
<ul class="sectlevel2">
<li><a href="#_installation_des_dépendances_systèmes">10.1. Installation des dépendances systèmes</a></li>
<li><a href="#_préparation_de_lenvironnement_utilisateur">10.2. Préparation de l&#8217;environnement utilisateur</a></li>
<li><a href="#_configuration_de_lapplication">10.3. Configuration de l&#8217;application</a></li>
<li><a href="#_création_des_répertoires_de_logs">10.4. Création des répertoires de logs</a></li>
<li><a href="#_création_du_répertoire_pour_le_socket">10.5. Création du répertoire pour le socket</a></li>
<li><a href="#_gunicorn">10.6. Gunicorn</a></li>
<li><a href="#_supervision_2">10.7. Supervision</a></li>
<li><a href="#_ouverture_des_ports">10.8. Ouverture des ports</a></li>
<li><a href="#_installation_dnginx">10.9. Installation d&#8217;Nginx</a></li>
<li><a href="#_configuration_des_sauvegardes">10.10. Configuration des sauvegardes</a></li>
</ul>
</li>
<li><a href="#_postgresql">11. PostgreSQL</a></li>
<li><a href="#_mariadb">12. MariaDB</a></li>
</ul>
</li>
<li><a href="#_modélisation">Modélisation</a>
<ul class="sectlevel1">
<li><a href="#_modélisation_2">13. Modélisation</a></li>
<li><a href="#_queryset_managers">14. Queryset &amp; managers</a></li>
<li><a href="#_forms">15. Forms</a>
<ul class="sectlevel2">
<li><a href="#_dépendance_avec_le_modèle">15.1. Dépendance avec le modèle</a></li>
<li><a href="#_rendu_et_affichage">15.2. Rendu et affichage</a></li>
<li><a href="#_squelette_par_défaut">15.3. Squelette par défaut</a></li>
<li><a href="#_crispy_forms">15.4. Crispy-forms</a></li>
</ul>
</li>
<li><a href="#_validation_des_données">16. Validation des données</a></li>
<li><a href="#_en_conclusion">17. En conclusion</a></li>
<li><a href="#_migrations">18. Migrations</a></li>
<li><a href="#_modèle_vue_template">19. Modèle-vue-template</a>
<ul class="sectlevel2">
<li><a href="#_vues">19.1. Vues</a></li>
</ul>
</li>
<li><a href="#_logging">20. Logging</a></li>
<li><a href="#_administration">21. Administration</a>
<ul class="sectlevel2">
<li><a href="#_quelques_conseils">21.1. Quelques conseils</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#_go_live">Go Live !</a>
<ul class="sectlevel1">
<li><a href="#_besoins_utilisateurs">22. Besoins utilisateurs</a></li>
<li><a href="#_besoins_fonctionnels">23. Besoins fonctionnels</a>
<ul class="sectlevel2">
<li><a href="#_gestion_des_utilisateurs">23.1. Gestion des utilisateurs</a></li>
<li><a href="#_gestion_des_listes">23.2. Gestion des listes</a></li>
<li><a href="#_gestion_des_souhaits">23.3. Gestion des souhaits</a></li>
<li><a href="#_gestion_des_réalisations_de_souhaits">23.4. Gestion des réalisations de souhaits</a></li>
<li><a href="#_gestion_des_personnes_réalisants_les_souhaits_et_qui_ne_sont_pas_connues">23.5. Gestion des personnes réalisants les souhaits et qui ne sont pas connues</a></li>
</ul>
</li>
<li><a href="#_tests_unitaires_2">24. Tests unitaires</a>
<ul class="sectlevel2">
<li><a href="#_pourquoi_sennuyer_à_écrire_des_tests">24.1. Pourquoi s&#8217;ennuyer à écrire des tests?</a></li>
<li><a href="#_why_bother_with_test_discipline">24.2. Why Bother with Test Discipline?</a></li>
<li><a href="#_what_are_you_testing">24.3. What are you testing?</a></li>
<li><a href="#_couverture_de_code_2">24.4. Couverture de code</a></li>
<li><a href="#_comment_tester">24.5. Comment tester ?</a></li>
<li><a href="#_quelques_liens_utiles">24.6. Quelques liens utiles</a></li>
</ul>
</li>
<li><a href="#_a_retenir">25. A retenir</a>
<ul class="sectlevel2">
<li><a href="#_constructeurs">25.1. Constructeurs</a></li>
<li><a href="#_relations">25.2. Relations</a></li>
<li><a href="#_querysets_managers">25.3. Querysets &amp; managers</a></li>
</ul>
</li>
<li><a href="#_refactoring">26. Refactoring</a>
<ul class="sectlevel2">
<li><a href="#_classe_abstraite">26.1. Classe abstraite</a></li>
<li><a href="#_héritage_classique">26.2. Héritage classique</a></li>
<li><a href="#_classe_proxy">26.3. Classe proxy</a></li>
<li><a href="#_métamodèle">Métamodèle</a></li>
</ul>
</li>
<li><a href="#_supervision_des_logs">27. Supervision des logs</a></li>
<li><a href="#_feedbacks_utilisateurs">28. feedbacks utilisateurs</a></li>
</ul>
</li>
<li><a href="#_en_bonus">En bonus</a>
<ul class="sectlevel1">
<li><a href="#_snippets_utiles_et_forcément_dispensables">29. Snippets utiles (et forcément dispensables)</a>
<ul class="sectlevel2">
<li><a href="#_récupération_du_dernier_tag_git_en_python">29.1. Récupération du dernier tag Git en Python</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div id="content">
<div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<p>On ne va pas se mentir: il existe enormément de tutoriaux très bien réalisés sur "Comment réaliser une application Django" et autres "Déployer votre code en 2 minutes". On se disait juste que ces tutoriaux restaient relativement haut-niveau et se limitaient à un contexte donné.</p>
</div>
<div class="paragraph">
<p>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 (du développement au déploiement) et de s&#8217;assurer du maintient correct de la base de code, en permettant à n&#8217;importe qui de reprendre le développement.</p>
</div>
<div class="paragraph">
<p>Ces idées ne s&#8217;appliquent pas uniquement à Django et à son cadre de travail, ni même au langage Python. Juste que ces deux bidules sont de bons candidats et que le cadre de travail est bien défini et suffisamment flexible.</p>
</div>
<div class="paragraph">
<p>Django se présente comme un `Framework Web pour perfectionnistes ayant des deadlines &lt;<a href="https://www.djangoproject.com/&gt;`_" class="bare">https://www.djangoproject.com/&gt;`_</a>.</p>
</div>
<div class="paragraph">
<p>Django suit quelques principes &lt;<a href="https://docs.djangoproject.com/en/dev/misc/design-philosophies/&gt;" class="bare">https://docs.djangoproject.com/en/dev/misc/design-philosophies/&gt;</a>:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Faible couplage et forte cohésion, pour que chaque composant ait son indépendance.</p>
</li>
<li>
<p>Moins de code, plus de fonctionnalités.</p>
</li>
<li>
<p>`Don&#8217;t repeat yourself &lt;<a href="https://fr.wikipedia.org/wiki/Sec&gt;`_" class="bare">https://fr.wikipedia.org/wiki/Sec&gt;`_</a>: ne pas se répéter!</p>
</li>
<li>
<p>Rapidité du développement (après une courbe d&#8217;apprentissage relativement ardue, malgré tout)</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Mis côté à côté, l&#8217;application de ces principes permet une meilleure stabilité du projet. Dans la suite de ce chapitre, on verra comment configurer l&#8217;environnement, comment installer Django de manière isolée et comment démarrer un nouveau projet. On verra comment gérer correctement les dépendances, les versions et comment applique un score sur note code.</p>
</div>
<div class="paragraph">
<p>Finalement, on verra aussique la configuration proposée par défaut par le framework n&#8217;est pas idéale pour la majorité des cas.</p>
</div>
<div class="paragraph">
<p>Pour cela, on présentera différents outils (mypy, flake8, black, &#8230;&#8203;), 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, ainsi que les bonnes étapes à suivre pour arriver à un déploiement rapide et fonctionnel avec peu d&#8217;efforts.</p>
</div>
<div class="paragraph">
<p>Et tout ça à un seul et même endroit. Oui. :-)</p>
</div>
<div class="paragraph">
<p>Bonne lecture.</p>
</div>
</div>
</div>
<h1 id="_environnement_de_travail" class="sect0">Environnement de travail</h1>
<div class="openblock partintro">
<div class="content">
<div class="paragraph">
<p>Avant de démarrer le développement, il est nécessaire de passer un peu de temps sur la configuration de l&#8217;environnement.</p>
</div>
<div class="paragraph">
<p>Les morceaux de code seront développés pour Python3.4+ et Django 1.8+. Ils nécessiteront peut-être quelques adaptations pour fonctionner sur une version antérieure.</p>
</div>
<div class="paragraph">
<p><strong>Remarque</strong> : les commandes qui seront exécutés dans ce livre le seront depuis un shell sous GNU/Linux. Certaines devront donc être adaptées si vous êtes dans un autre environnemnet.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_méthodologie_de_travail">1. Méthodologie de travail</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Pour la méthode de travail et de développement, on va se baser sur les <a href="https://12factor.net/fr/">The Twelve-factor App</a> - ou plus simplement les <strong>12 facteurs</strong>.</p>
</div>
<div class="paragraph">
<p>L&#8217;idée derrière cette méthode consiste à pousser les concepts suivants (repris grossièrement de la <a href="https://12factor.net/fr/">page d&#8217;introduction</a> :</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Faciliter la mise en place de phases d&#8217;automatisation; plus simplement, 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;<a href="https://en.wikipedia.org/wiki/Continuous_integration">intégration continue</a>/<a href="https://en.wikipedia.org/wiki/Continuous_deployment">déploiement continu</a></p>
</li>
<li>
<p>Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet.</p>
</li>
<li>
<p>Faciliter</p>
</li>
<li>
<p>Augmenter l&#8217;agilité générale du projet, en permettant une meilleure évolutivité architecturale et une meilleure mise à l&#8217;échelle - <em>Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on n&#8217;en parle plus ;-)</em>.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>En pratique, les idées planquées derrière les quelques phrases ci-dessus permettront de monter facilement un nouvel environnement - qu&#8217;il soit sur la machine du petit nouveau ou sur un serveur Azure, Heroku, Digital Ocean ou votre nouveau Raspberry Pi Zéro caché à la cave.</p>
</div>
<div class="paragraph">
<p>Pour reprendre de manière très brute les différentes idées derrière cette méthode, on a:</p>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
pensez à retravailler la partie ci-dessous; la version anglophone semble plus compréhensible&#8230;&#8203; :-/
</td>
</tr>
</table>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Une base de code suivie avec un système de contrôle de version, plusieurs déploiements</p>
</li>
<li>
<p>Déclarez explicitement et isolez les dépendances</p>
</li>
<li>
<p>Stockez la configuration dans lenvironnement</p>
</li>
<li>
<p>Traitez les services externes comme des ressources attachées</p>
</li>
<li>
<p>Séparez strictement les étapes dassemblage et dexécution</p>
</li>
<li>
<p>Exécutez lapplication comme un ou plusieurs processus sans état</p>
</li>
<li>
<p>Exportez les services via des associations de ports</p>
</li>
<li>
<p>Grossissez à laide du modèle de processus</p>
</li>
<li>
<p>Maximisez la robustesse avec des démarrages rapides et des arrêts gracieux</p>
</li>
<li>
<p>Gardez le développement, la validation et la production aussi proches que possible</p>
</li>
<li>
<p>Traitez les logs comme des flux dévènements</p>
</li>
<li>
<p>Lancez les processus dadministration et de maintenance comme des one-off-processes</p>
</li>
</ol>
</div>
<table class="tableblock frame-all grid-all stretch">
<caption class="title">Table 1. Concrètement</caption>
<colgroup>
<col style="width: 33.3333%;">
<col style="width: 33.3333%;">
<col style="width: 33.3334%;">
</colgroup>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Concept</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Outil</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Description</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Base de code suivie avec un système de contrôle de version</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Git, Mercurial, SVN, &#8230;&#8203;</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Chaque déploiement démarre à partir d&#8217;une base de code unique. Il n&#8217;y pas de dépôt "Prod", "Staging" ou "Dev". Il n&#8217;y en a qu&#8217;un et un seul.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Déclaration explicite et isolation des dépendances</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Pyenv, environnements virtuels, RVM, &#8230;&#8203;</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Afin de ne pas perturber les dépendances systèmes, chaque application doit disposer d&#8217;un environnement sain par défaut.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Configuration dans l&#8217;environnement</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Fichiers .ENV</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">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, on ne doit 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.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Services externes = ressources locales</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Fichiers .ENV</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Chaque ressource doit pouvoir être interchangeable avec une autre, sans modification du code source. La solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d&#8217;authentification, &#8230;&#8203;) directement via des variables d&#8217;environnement.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Bien séparer les étapes de construction des étapes de mise à disposition</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Capistrano, Gitea, un serveur d&#8217;artefacts, &#8230;&#8203;</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">L&#8217;idée est de pouvoir récupérer une version spécifique du code, sans que celle-ci ne puisse avoir été modifiée. Git permet bien de gérer des versions (au travers des tags), mais ces éléments peuvent sans doute être modifiés directement au travers de l&#8217;historique.</p></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="sect1">
<h2 id="_environnements_virtuels">2. Environnements virtuels</h2>
<div class="sectionbody">
<div class="paragraph">
<p>On va commencer avec la partie la moins funky, mais la plus utile, dans la vie d&#8217;un développeur: la gestion et l&#8217;isolation des dépendances.</p>
</div>
<div class="paragraph">
<p>Il est tout à fait possible de s&#8217;en passer complètement dans le cadre de "petits" projets ou d&#8217;applications déployées sur des machines dédiées, et de fonctionner à grand renforts de "sudo" et d&#8217;installation globale des dépendances. Cette pratique est cependant fortement déconseillée pour plusieurs raisons:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Pour la reproductibilité d&#8217;un environnement spécifique. Cela évite notamment les réponses type "Ca juste marche chez moi", puisqu&#8217;on a la possibilité de construire un environnement sain et appliquer des dépendances identiques, quelle que soit la machine hôte.</p>
</li>
<li>
<p>Il est tout à fait envisagable que deux applications soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d&#8217;une même dépendance.</p>
</li>
</ol>
</div>
<div class="quoteblock">
<blockquote>
<div class="paragraph">
<p>But it works on my machine! Then, we&#8217;ll ship your machine.</p>
</div>
</blockquote>
<div class="attribution">
&#8212; A famous meme<br>
<cite>And this is how Docker was born.</cite>
</div>
</div>
<div class="paragraph">
<p>Nous allons utiliser le module <code>venv</code> afin de créer un `environnement virtuel &lt;<a href="http://sametmax.com/les-environnement-virtuels-python-virtualenv-et-virtualenvwrapper/&gt;`_" class="bare">http://sametmax.com/les-environnement-virtuels-python-virtualenv-et-virtualenvwrapper/&gt;`_</a> pour python.</p>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
auparavant, on utilisait <code>virtualenvwrapper</code>. mais cela fait plusieurs années que je ne l&#8217;utilise plus. On l&#8217;intègre quand même ?
</td>
</tr>
</table>
</div>
<div class="sect2">
<h3 id="_création_de_lenvironnement_virtuel">2.1. Création de l&#8217;environnement virtuel</h3>
<div class="paragraph">
<p>Commencons par créer un environnement virtuel, afin d&#8217;y stocker les dépendances. Lancez <code>python3 -m venv gwift-env</code>.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">---
Intégrer les résultats de la création de l'environnement
---</code></pre>
</div>
</div>
<div class="paragraph">
<p>Ceci créera l&#8217;arborescence de fichiers suivante, qui peut à nouveau être un peu différente en fonction du système d&#8217;exploitation:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre>$ ls .virtualenvs/gwift-env
bin include lib</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Nous pouvons ensuite l&#8217;activer grâce à la commande <code>source gwift-env</code>.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">---
Intégrer les résultats de l'accès de l'environnement
---</code></pre>
</div>
</div>
<div class="paragraph">
<p>Le <strong>shell</strong> signale que nous sommes bien dans l&#8217;environnement <code>gwift-env</code> en l&#8217;affichant avant le <code>$</code>. Par la suite, nous considérerons que l&#8217;environnement virtuel est toujours activé, même si <code>gwift-env</code> n&#8217;est pas présent devant chaque <code>$</code>.</p>
</div>
<div class="paragraph">
<p>A présent, tous les binaires de cet environnement prendront le pas sur les binaires du système. De la même manière, une variable <code>PATH</code> 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.</p>
</div>
<div class="paragraph">
<p>Pour désactiver l&#8217;environnement virtuel, il suffira d&#8217;utiliser la commande <code>deactivate</code></p>
</div>
</div>
<div class="sect2">
<h3 id="_installation_de_django_et_création_du_répertoire_de_travail">2.2. Installation de Django et création du répertoire de travail</h3>
<div class="paragraph">
<p>Comme l&#8217;environnement est activé, on peut à présent y installer Django. La librairie restera indépendante du reste du système, et ne polluera pas les autres projets.</p>
</div>
<div class="paragraph">
<p>C&#8217;est parti: <code>pip install django</code>!</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre>$ pip install django
Collecting django
Downloading Django-X.Y.Z
100% |################################|
Installing collected packages: django
Successfully installed django-X.Y.Z</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Les commandes de création d&#8217;un nouveau site sont à présent disponibles, la principale étant <code>django-admin startproject</code>. Par la suite, nous utiliserons <code>manage.py</code>, qui constitue un <strong>wrapper</strong> autour de <code>django-admin</code>.</p>
</div>
<div class="paragraph">
<p>Pour démarrer notre projet, nous lançons donc <code>django-admin startproject gwift</code>.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ django-admin startproject gwift</code></pre>
</div>
</div>
<div class="paragraph">
<p>Cette action a pour effet de créer un nouveau dossier <code>gwift</code>, dans lequel on trouve la structure suivante:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ tree gwift
gwift
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py</code></pre>
</div>
</div>
<div class="paragraph">
<p>C&#8217;est sans ce répertoire que vont vivre tous les fichiers liés au projet. Le but est de faire en sorte que toutes les opérations (maintenance, déploiement, écriture, tests, &#8230;&#8203;) puissent se faire à partir d&#8217;un seul point d&#8217;entrée. Tant qu&#8217;on y est, nous pouvons rajouter les répertoires utiles à la gestion de notre projet, à savoir la documentation, les dépendances et le README:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ mkdir docs requirements
$ touch docs/README.md</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ tree gwift
gwift
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
|-- docs/
|-- requirements/
|-- README</code></pre>
</div>
</div>
<div class="paragraph">
<p>Chacun de ces fichiers sert à:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>settings.py</code> contient tous les paramètres globaux à notre projet.</p>
</li>
<li>
<p><code>urls.py</code> contient les variables de routes, les adresses utilisées et les fonctions vers lesquelles elles pointent.</p>
</li>
<li>
<p><code>manage.py</code>, pour toutes les commandes de gestion.</p>
</li>
<li>
<p><code>wsgi.py</code> contient la définition de l&#8217;interface `WSGI &lt;<a href="https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface&gt;`_" class="bare">https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface&gt;`_</a>, qui permettra à votre serveur Web (Nginx, Apache, &#8230;&#8203;) de faire un pont vers votre projet.</p>
</li>
</ul>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
déplacer la configuration dans un répertoire <code>config</code> à part.
</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="_gestion_des_dépendances">2.3. Gestion des dépendances</h3>
<div class="paragraph">
<p>Comme nous venons d&#8217;ajouter une dépendance à notre projet, nous allons créer un fichier reprenant tous les dépendances de notre projet. Celles-ci sont normalement placées dans un fichier <code>requirements.txt</code>. 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 (<code>requirements</code>), afin de grouper les dépendances en fonction de leur utilité:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>base.txt</code></p>
</li>
<li>
<p><code>dev.txt</code></p>
</li>
<li>
<p><code>staging.txt</code></p>
</li>
<li>
<p><code>production.txt</code></p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Au début de chaque fichier, il suffira d&#8217;ajouter la ligne <code>-r base.txt</code>, puis de lancer l&#8217;installation grâce à un <code>pip install -r &lt;nom du fichier&gt;</code>. De cette manière, il est tout à fait acceptable de n&#8217;installer <code>flake8</code> et <code>django-debug-toolbar</code> qu&#8217;en développement par exemple. Dans l&#8217;immédiat, ajoutez simplement <code>django</code> dans le fichier <code>requirements/base.txt</code>.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ echo django &gt;&gt; requirements/base.txt</code></pre>
</div>
</div>
<div class="paragraph">
<p>Par la suite, il vous faudra <strong>absolument</strong> spécifier les versions à utiliser: les librairies que vous utilisez comme dépendances évoluent, de la même manière que vos projets. Des fonctions sont cassées, certaines signatures sont modifiées, des comportements sont altérés, etc. Si vous voulez être sûr et certain que le code que vous avez écrit continue à fonctionner, spécifiez la version de chaque librairie de dépendances. Avec les mécanismes d&#8217;intégration continue et de tests unitaires, on verra plus loin comment se prémunir d&#8217;un changement inattendu.</p>
</div>
</div>
<div class="sect2">
<h3 id="_structure_finale_de_lenvironnement">2.4. Structure finale de l&#8217;environnement</h3>
<div class="paragraph">
<p>Nous avons donc la strucutre finale pour notre environnement de travail:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ tree ~/gwift-project
gwift
├── docs
│   └── README.md
├── gwift
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
│   manage.py
└── requirements
└── base.txt</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_django">3. Django</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Comme on l&#8217;a vu ci-dessus, <code>django-admin</code> permet de créer un nouveau projet. On fait ici une distinction entre un <strong>projet</strong> et une <strong>application</strong>:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>Projet</strong>: ensemble des applications, paramètres, pages HTML, middlwares, dépendances, etc., qui font que votre code fait ce qu&#8217;il est sensé faire.</p>
</li>
<li>
<p><strong>Application</strong>: <strong>contexte</strong> éventuellement indépendant, permettant d&#8217;effectuer une partie isolée de ce que l&#8217;on veut faire.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Pour <code>gwift</code>, on va notamment avoir une application pour la gestion des listes de souhaits et des éléments, une deuxième application pour la gestion des utilisateurs, voire une troisième application qui gérera les partages entre utilisateurs et listes.</p>
</div>
<div class="paragraph">
<p>On voit bien ici le principe de <strong>contexte</strong>: l&#8217;application viendra avec son modèle, ses tests, ses vues et son paramétrage. Elle pourra éventuellement être réutilisée dans un autre projet. C&#8217;est en ça que consistent les `paquets Django &lt;<a href="https://www.djangopackages.com/&gt;`_" class="bare">https://www.djangopackages.com/&gt;`_</a> déjà disponibles: ce sont simplement de petites applications empaquetées pour être réutilisées (eg. `Django-Rest-Framework &lt;<a href="https://github.com/tomchristie/django-rest-framework&gt;`" class="bare">https://github.com/tomchristie/django-rest-framework&gt;`</a><em>, `Django-Debug-Toolbar &lt;<a href="https://github.com/django-debug-toolbar/django-debug-toolbar&gt;`" class="bare">https://github.com/django-debug-toolbar/django-debug-toolbar&gt;`</a></em>, &#8230;&#8203;).</p>
</div>
<div class="sect2">
<h3 id="_gestion">3.1. Gestion</h3>
<div class="paragraph">
<p>Comme expliqué un peu plus haut, le fichier <code>manage.py</code> est un <strong>wrapper</strong> sur les commandes <code>django-admin</code>. A partir de maintenant, nous n&#8217;utiliserons plus que celui-là pour tout ce qui touchera à la gestion de notre projet:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>manage.py check</code> pour vérifier (en surface&#8230;&#8203;) que votre projet ne rencontre aucune erreur</p>
</li>
<li>
<p><code>manage.py runserver</code> pour lancer un serveur de développement</p>
</li>
<li>
<p><code>manage.py test</code> pour découvrir les tests unitaires disponibles et les lancer.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>La liste complète peut être affichée avec <code>manage.py help</code>. Vous remarquerez que ces commandes sont groupées selon différentes catégories:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>auth</strong>: création d&#8217;un nouveau super-utilisateur, changer le mot de passe pour un utilisateur existant.</p>
</li>
<li>
<p><strong>django</strong>: vérifier la <strong>compliance</strong> du projet, lancer un <strong>shell</strong>, <strong>dumper</strong> les données de la base, effectuer une migration du schéma, &#8230;&#8203;</p>
</li>
<li>
<p><strong>sessions</strong>: suppressions des sessions en cours</p>
</li>
<li>
<p><strong>staticfiles</strong>: gestion des fichiers statiques et lancement du serveur de développement.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Nous verrons plus tard comment ajouter de nouvelles commandes.</p>
</div>
</div>
<div class="sect2">
<h3 id="_structure_dune_application">3.2. Structure d&#8217;une application</h3>
<div class="paragraph">
<p>Maintenant que l&#8217;on a vu à quoi servait <code>manage.py</code>, on peut créer notre nouvelle application grâce à la commande <code>manage.py startapp &lt;label&gt;</code>.</p>
</div>
<div class="paragraph">
<p>Cette application servira à structurer les listes de souhaits, les éléments qui les composent et les parties que chaque utilisateur pourra offrir. Essayez de trouver un nom éloquent, court et qui résume bien ce que fait l&#8217;application. Pour nous, ce sera donc <code>wish</code>. C&#8217;est parti pour <code>manage.py startapp wish</code>!</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ python manage.py startapp wish</code></pre>
</div>
</div>
<div class="paragraph">
<p>Résultat? Django nous a créé un répertoire <code>wish</code>, dans lequel on trouve les fichiers suivants:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ ls -l wish
admin.py __init__.py migrations models.py tests.py views.py</code></pre>
</div>
</div>
<div class="paragraph">
<p>En résumé, chaque fichier a la fonction suivante:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>admin.py</code> servira à structurer l&#8217;administration de notre application. Chaque information peut en effet être administrée facilement au travers d&#8217;une interface générée à la volée par le framework. On y reviendra par la suite.</p>
</li>
<li>
<p><code><em>init</em>.py</code> pour que notre répertoire <code>wish</code> soit converti en package Python.</p>
</li>
<li>
<p><code>migrations/</code>, dossier dans lequel seront stockées toutes les différentes migrations de notre application.</p>
</li>
<li>
<p><code>models.py</code> pour représenter et structurer nos données.</p>
</li>
<li>
<p><code>tests.py</code> pour les tests unitaires.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_tests_unitaires">3.3. Tests unitaires</h3>
<div class="paragraph">
<p>Plein de trucs à compléter ici ;-) Est-ce qu&#8217;on passe par pytest ou par le framework intégré ? Quels sont les avantages de l&#8217;un % à l&#8217;autre ?
* <code>views.py</code> pour définir ce que nous pouvons faire avec nos données.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_construire_des_applications_maintenables">4. Construire des applications maintenables</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Pour cette section, je me base d&#8217;un résumé de l&#8217;ebook <strong>Building Maintenable Software</strong> disponible chez `O&#8217;Reilly &lt;<a href="http://shop.oreilly.com/product/0636920049555.do`_" class="bare">http://shop.oreilly.com/product/0636920049555.do`_</a> qui vaut clairement le détour pour poser les bases d&#8217;un projet.</p>
</div>
<div class="paragraph">
<p>Ce livre répartit un ensemble de conseils parmi quatre niveaux de composants:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Les méthodes et fonctions</p>
</li>
<li>
<p>Les classes</p>
</li>
<li>
<p>Les composants</p>
</li>
<li>
<p>Et de manière plus générale.</p>
</li>
</ul>
</div>
<div class="sect2">
<h3 id="_au_niveau_des_méthodes_et_fonctions">4.1. Au niveau des méthodes et fonctions</h3>
<div class="ulist">
<ul>
<li>
<p>Gardez vos méthodes/fonctions courtes. Pas plus de 15 lignes, en comptant les commentaires. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9% de plus de 60 lignes; pas plus de 22.3% de plus de 30 lignes, au plus 43.7% de plus de 15 lignes et au moins 56.3% en dessous de 15 lignes). Oui, c&#8217;est dur à tenir, mais faisable.</p>
</li>
<li>
<p>Conserver une complexité de McCabe en dessous de 5, c&#8217;est-à-dire avec quatre branches au maximum. A nouveau, si on a une méthode avec une complexité cyclomatique de 15, la séparer en 3 fonctions avec une complexité de 5 conservera globalement le nombre 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.</p>
</li>
<li>
<p>N&#8217;écrivez votre code qu&#8217;une seule fois: évitez les duplications, copie, etc., 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.</p>
</li>
<li>
<p>Conservez de petites interfaces. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe, plus facile à tester.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_au_niveau_des_classes">4.2. Au niveau des classes</h3>
<div class="ulist">
<ul>
<li>
<p>Privilégiez un couplage faible entre vos classes. Ceci n&#8217;est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences. L&#8217;implémentation du service <code>UserNotificationsService</code> ne doit pas forcément se trouver embarqué dans une classe <code>UserService</code>. 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.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_au_niveau_des_composants">4.3. Au niveau des composants</h3>
<div class="ulist">
<ul>
<li>
<p>Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants é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;on ne puisse 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, "juste parce qu&#8217;elle en a la possibilité". Vous pourrez é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 <code>IFileTransfer</code> avec ses méthodes <code>put</code> et <code>get</code>, et non pas les détails d&#8217;implémentation complet d&#8217;une classe <code>FtpFileTransfer</code> ou <code>SshFileTransfer</code>).</p>
</li>
<li>
<p>Conserver un bon balancement au niveau des composants: évitez qu&#8217;un composant <strong>A</strong> ne soit un énorme mastodonte, alors que le composant juste à côté n&#8217;est 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 plus faciles à gérer. Un conseil est d&#8217;avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que ces composants soit approximativement de même taille.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_de_manière_plus_générale">4.4. De manière plus générale</h3>
<div class="ulist">
<ul>
<li>
<p>Conserver une densité de code faible: 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.</p>
</li>
<li>
<p>Automatiser les tests, ajouter un environnement d&#8217;intégration continue dès le début du projet et vérifier par des outils les points ci-dessus.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_en_pratique">4.5. En pratique</h3>
<div class="paragraph">
<p>Par rapport aux points repris ci-dessus, l&#8217;environnement Python et le framework Django proposent un ensemble d&#8217;outils intégrés qui permettent de répondre à chaque point. Avant d&#8217;aller plus loin, donc, un petit point sur les conventions, les tests (unitaires, orientés comportement, basés sur la documentation, &#8230;&#8203;), la gestion de version du code et sur la documentation. Plus que dans tout langage compilé, ceux-ci sont pratiquement obligatoires. Vous pourrez les voir comme une perte de temps dans un premier temps, mais nous vous promettons qu&#8217;ils vous en feront gagner par la suite.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_chaîne_doutils">5. Chaîne d&#8217;outils</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Le langage Python fonctionne avec un système daméliorations basées sur des propositions: les PEP, ou “Python Enhancement Proposal”.</p>
</div>
<div class="paragraph">
<p>Celle qui va nous intéresser pour cette section est la <a href="https://www.python.org/dev/peps/pep-0008/">PEP 8&#8201;&#8212;&#8201;Style Guide for Python Code</a>. Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour lindentation, le nommage des variables et des classes, … En bref, elle décrit comment écrire du code proprement pour que dautres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.</p>
</div>
<div class="sect2">
<h3 id="_pep8">5.1. PEP8</h3>
<div class="paragraph">
<p>Le langage Python fonctionne avec un système d&#8217;améliorations basées sur des propositions: les PEP, ou "<strong>Python Enhancement Proposal</strong>". Chacune d&#8217;entre elles doit être approuvée par le `Benevolent Dictator For Life &lt;<a href="http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life&gt;`_" class="bare">http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life&gt;`_</a>.</p>
</div>
<div class="paragraph">
<p>La PEP qui nous intéresse plus particulièrement pour la suite est la `PEP-8 &lt;<a href="https://www.python.org/dev/peps/pep-0008/&gt;`_" class="bare">https://www.python.org/dev/peps/pep-0008/&gt;`_</a>, ou "Style Guide for Python Code". Elle spécifie des conventions d&#8217;organisation et de formatage de code Python, quelles sont les conventions pour l&#8217;indentation, le nommage des variables et des classes, etc. En bref, elle décrit comment écrire du code proprement pour que d&#8217;autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.</p>
</div>
<div class="paragraph">
<p>Sur cette base, 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 (<code>.</code>, le nom d&#8217;un répertoire, le nom d&#8217;un fichier <code>.py</code>, &#8230;&#8203;). Si vous souhaitez uniquement avoir le nombre d&#8217;erreur de chaque type, saisissez les options <code>--statistics -qq</code>.</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre>$ pep8 . --statistics -qq</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>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 ':'</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>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 <code>pyflakes</code>: il analysera vos sources à la recherche de sources d&#8217;erreurs possibles (imports inutilisés, méthodes inconnues, etc.).</p>
</div>
<div class="paragraph">
<p>Finalement, la solution qui couvre ces deux domaines existe et s&#8217;intitule `flake8 &lt;<a href="https://github.com/PyCQA/flake8&gt;`_" class="bare">https://github.com/PyCQA/flake8&gt;`_</a>. Sur base la même interface que <code>pep8</code>, vous aurez en plus tous les avantages liés à <code>pyflakes</code> concernant votre code source.</p>
</div>
<div class="sect3">
<h4 id="_pep257">5.1.1. PEP257</h4>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>todo:: à remplir avec <code>pydocstyle</code>.</p>
</li>
</ol>
</div>
</div>
<div class="sect3">
<h4 id="_tests">5.1.2. Tests</h4>
<div class="paragraph">
<p>Comme tout bon <strong>framework</strong> qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier <strong>tests.py</strong>, qui inclut la classe <code>TestCase</code> depuis le package <code>django.test</code>:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre>from django.test import TestCase</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>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é.</p>
</div>
</div>
<div class="sect3">
<h4 id="_couverture_de_code">5.1.3. Couverture de code</h4>
<div class="paragraph">
<p>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 <strong>bien</strong> testé, mais juste de vérifier <strong>quelle partie</strong> du code est testée. En Python, il existe le paquet `coverage &lt;<a href="https://pypi.python.org/pypi/coverage/&gt;`_" class="bare">https://pypi.python.org/pypi/coverage/&gt;`_</a>, qui se charge d&#8217;évaluer le pourcentage de code couvert par les tests. Ajoutez-le dans le fichier <code>requirements/base.txt</code>, et lancez une couverture de code grâce à la commande <code>coverage</code>. La configuration peut se faire dans un fichier <code>.coveragerc</code> que vous placerez à la racine de votre projet, et qui sera lu lors de l&#8217;exécution.</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre># requirements/base.text
[...]
coverage
django_coverage_plugin</pre>
</div>
</div>
</li>
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre># .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>[report]
ignore_errors = True</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>[html]
directory = coverage_html_report</pre>
</div>
</div>
</li>
<li>
<p>todo:: le bloc ci-dessous est à revoir pour isoler la configuration.</p>
</li>
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre>$ coverage run --source "." manage.py test
$ coverage report</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>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%</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>$ coverage html</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. Pour gagner un peu de temps, n&#8217;hésitez pas à créer un fichier <code>Makefile</code> que vous placerez à la racine du projet. L&#8217;exemple ci-dessous permettra, grâce à la commande <code>make coverage</code>, d&#8217;arriver au même résultat que ci-dessus:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre># Makefile for gwift
#</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre># 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</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>.PHONY: help coverage</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>help:
@echo " coverage to run coverage check of the source files."</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."</pre>
</div>
</div>
</li>
</ol>
</div>
</div>
<div class="sect3">
<h4 id="_complexité_de_mccabe">5.1.4. Complexité de McCabe</h4>
<div class="paragraph">
<p>La `complexité cyclomatique &lt;<a href="https://fr.wikipedia.org/wiki/Nombre_cyclomatique&gt;`_" class="bare">https://fr.wikipedia.org/wiki/Nombre_cyclomatique&gt;`_</a> (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. Par exemple:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre>if True == True:
pass # never happens</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre># continue ...</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>La condition existe, mais on ne passera jamais dedans. A l&#8217;inverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre>def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusqu&#8217;à arriver à l&#8217;impression à l&#8217;écran et à la valeur de retour. 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 on rencontre une condition, elle passera à 2, etc.</p>
</div>
<div class="paragraph">
<p>Pour l&#8217;exemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour s&#8217;assurer que la couverture est complète. On devrait donc trouver:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Un test pour entrer (ou non) dans la condition <code>a == b</code></p>
</li>
<li>
<p>Un test pour entrer (ou non) dans la condition <code>b == c</code></p>
</li>
<li>
<p>Un test pour entrer (ou non) dans la condition <code>c == d</code></p>
</li>
<li>
<p>Un test pour entrer (ou non) dans la condition <code>d == e</code></p>
</li>
<li>
<p>Et s&#8217;assurer que n&#8217;importe quel autre cas retournera la valeur <code>None</code>.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>On a donc bien besoin de minimum cinq tests pour couvrir l&#8217;entièreté des cas présentés.</p>
</div>
<div class="paragraph">
<p>Le nombre de tests unitaires nécessaires à la couverture d&#8217;un bloc est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d&#8217;autres de 5.</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>note::</p>
<div class="literalblock">
<div class="content">
<pre>Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>A nouveau, un greffon pour <code>flake8</code> existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec <code>pip install mccabe</code>, et activez-le avec le paramètre <code>--max-complexity</code>. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.</p>
</div>
</div>
<div class="sect3">
<h4 id="_documentation">5.1.5. Documentation</h4>
<div class="paragraph">
<p>Il existe plusieurs manières de générer la documentation d&#8217;un projet. Les plus connues sont `Sphinx &lt;<a href="http://sphinx-doc.org/&gt;`_" class="bare">http://sphinx-doc.org/&gt;`_</a> et `MkDocs &lt;<a href="http://www.mkdocs.org/&gt;`" class="bare">http://www.mkdocs.org/&gt;`</a><em>. Le premier a l&#8217;avantage d&#8217;être plus reconnu dans la communauté Python que l&#8217;autre, de pouvoir <strong>parser</strong> le code pour en extraire la documentation et de pouvoir lancer des `tests orientés documentation &lt;<a href="https://duckduckgo.com/?q=documentation+driven+development&amp;t=ffsb&gt;`" class="bare">https://duckduckgo.com/?q=documentation+driven+development&amp;t=ffsb&gt;`</a></em>. A contrario, votre syntaxe devra respecter `ReStructuredText &lt;<a href="https://en.wikipedia.org/wiki/ReStructuredText&gt;`_" class="bare">https://en.wikipedia.org/wiki/ReStructuredText&gt;`_</a>. Le second a l&#8217;avantage d&#8217;avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans son résultat.</p>
</div>
<div class="paragraph">
<p>Dans l&#8217;immédiat, nous nous contenterons d&#8217;avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/&#8230;&#8203;). Dans la continuié de <code>Flake8</code>, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>note::</p>
<div class="literalblock">
<div class="content">
<pre>voir si il ne faudrait pas mieux passer par pydocstyle.</pre>
</div>
</div>
</li>
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre>$ pip install flake8_docstrings</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Lancez ensuite <code>flake8</code> avec la commande <code>flake8 . --exclude="migrations"</code>. Sur notre projet (presque) vide, le résultat sera le suivant:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: shell</p>
<div class="literalblock">
<div class="content">
<pre>$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Bref, on le voit: nous n&#8217;avons que très peu de modules, et aucun d&#8217;eux n&#8217;est commenté.</p>
</div>
<div class="paragraph">
<p>En plus de cette méthode, Django permet également de rendre la documentation accessible depuis son interface d&#8217;administration.</p>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_pep8_flake8_pylint">5.2. pep8, flake8, pylint</h3>
<div class="paragraph">
<p>Un outil existe et listera lensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour linstaller, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (., le nom dun répertoire, le nom dun fichier .py, &#8230;&#8203;).</p>
</div>
<div class="paragraph">
<p>Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez pyflakes: il analaysera vos sources à la recherche derreurs (imports inutilsés, méthodes inconnues, etc.).</p>
</div>
<div class="paragraph">
<p>Et finalement, si vous voulez grouper les deux, il existe flake8. Sur base la même interface que pep8, vous aurez en plus des avertissements concernant le code source.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">from datetime import datetime
"""On stocke la date du jour dans la variable ToD4y"""
ToD4y = datetime.today()
def print_today(ToD4y):
today = ToD4y
print(ToD4y)
def GetToday():
return ToD4y
if __name__ == "__main__":
t = Get_Today()
print(t)</code></pre>
</div>
</div>
<div class="paragraph">
<p>L&#8217;exécution de la commande flake8 . retourne ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
test.py:18:1: W391 blank line at end of file</code></pre>
</div>
</div>
<div class="paragraph">
<p>On trouve des erreurs:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>de <strong>conventions</strong>: 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 <em>erreurs</em> 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.</p>
</li>
<li>
<p>de <strong>définition</strong>: une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug potentiel.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>L&#8217;étape d&#8217;après consiste à invoquer pylint. Lui, il est directement moins conciliant:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-text" data-lang="text">$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10</code></pre>
</div>
</div>
<div class="paragraph">
<p>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, on trouve des problèmes liés:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)</p>
</li>
<li>
<p>à des variables non définies (E0602)</p>
</li>
<li>
<p>de la documentation manquante (C0114, C0116)</p>
</li>
<li>
<p>de la redéfinition de variables (W0621).</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Pour reprendre la <a href="http://pylint.pycqa.org/en/latest/user_guide/message-control.html">documentation</a>, chaque code possède sa signification (ouf!):</p>
</div>
<div class="ulist">
<ul>
<li>
<p>C convention related checks</p>
</li>
<li>
<p>R refactoring related checks</p>
</li>
<li>
<p>W various warnings</p>
</li>
<li>
<p>E errors, for probable bugs in the code</p>
</li>
<li>
<p>F fatal, if an error occurred which prevented pylint from doing further* processing.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>PyLint est la version <strong>++</strong>, pour ceux qui veulent un code propre et sans bavure.</p>
</div>
</div>
<div class="sect2">
<h3 id="_black">5.3. Black</h3>
</div>
<div class="sect2">
<h3 id="_pytest">5.4. pytest</h3>
</div>
<div class="sect2">
<h3 id="_mypy">5.5. mypy</h3>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_git">6. Git</h2>
<div class="sectionbody">
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
insérer ici une description de Gitflow + quelques exemples types création, ajout, suppression, historique, branches, &#8230;&#8203; et quelques schémas qui-vont-bien.
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Il existe plusiseurs outils permettant de gérer les versions du code, dont les plus connus sont `git &lt;<a href="https://git-scm.com/&gt;`_" class="bare">https://git-scm.com/&gt;`_</a> et `mercurial &lt;<a href="https://www.mercurial-scm.org/&gt;`_" class="bare">https://www.mercurial-scm.org/&gt;`_</a>.</p>
</div>
<div class="paragraph">
<p>Dans notre cas, nous utilisons git et hebergons le code et le livre directement sur le gitlab de `framasoft &lt;<a href="https://git.framasoft.org/&gt;`_" class="bare">https://git.framasoft.org/&gt;`_</a></p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_graphviz">7. graphviz</h2>
<div class="sectionbody">
<div class="paragraph">
<p>En utilisant django_extensions (! bien suivre les étapes d&#8217;installation !).</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-text" data-lang="text"> C:\app\graphviz\bin\dot.exe graph.dot -Tpng -o graph.png</code></pre>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_en_résumé">8. En résumé</h2>
<div class="sectionbody">
<div class="paragraph">
<p>En résumé, la création d&#8217;un <strong>nouveau</strong> projet Django demande plus ou moins toujours les mêmes actions:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Configurer un environnement virtuel</p>
</li>
<li>
<p>Installer les dépendances et les ajouter dans le fichier <code>requirements.txt</code></p>
</li>
<li>
<p>Configurer le fichier <code>settings.py</code></p>
</li>
</ol>
</div>
<div class="paragraph">
<p>C&#8217;est ici que le projet <a href="http://cookiecutter.readthedocs.io/en/latest/readme.html">CookieCutter</a> va être intéressant: les X premières étapes peuvent être <strong>bypassées</strong> par une simple commande.</p>
</div>
</div>
</div>
<h1 id="_déploiement" class="sect0">Déploiement</h1>
<div class="openblock partintro">
<div class="content">
Et sécurisation du serveur.
</div>
</div>
<div class="sect1">
<h2 id="_un_peu_de_théorie_les_principales_étapes">9. Un peu de théorie&#8230;&#8203; Les principales étapes</h2>
<div class="sectionbody">
<div class="paragraph">
<p>On va déjà parler de déploiement. Le serveur que django met à notre disposition est prévu uniquement pour le développement: inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, &#8230;&#8203;). De même, la base de donnée doit supporter plus qu&#8217;un seul utilisateur: SQLite fonctionne très bien dès lors qu&#8217;on se limite à un seul utilisateur&#8230;&#8203; Sur une application Web, il est plus que probable que vous rencontriez rapidement des erreurs de base de données verrouillée pour écriture par un autre processus. Il est donc plus que bénéfique de passer sur quelque chose de plus solide.</p>
</div>
<div class="paragraph">
<p>Si vous avez suivi les étapes jusqu&#8217;ici, vous devriez à peine disposer d&#8217;un espace de travail proprement configuré, d&#8217;un modèle relativement basique et d&#8217;une configuration avec une base de données simpliste. En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce que vous souhaitez au final.</p>
</div>
<div class="paragraph">
<p>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é sur un serveur. On risque d&#8217;avoir oublié une partie des désidérata, d&#8217;avoir zappé une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu&#8217;elles fonctionnent sur un environnement en particulier.</p>
</div>
<div class="paragraph">
<p>Aborder le déploiement maintenant permet également de rédiger dès le début les procédures d&#8217;installation, de mise à jour et de sauvegardes. 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.</p>
</div>
<div class="paragraph">
<p>Dans cette partie, on abordera les points suivants:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>La définition de l&#8217;infrastructure nécessaire à notre application</p>
</li>
<li>
<p>La configuration de l&#8217;hôte, qui hébergera l&#8217;application: dans une machine physique, virtuelle ou dans un container. On abordera aussi rapidement les déploiements via Ansible, Chef, Puppet ou Salt.</p>
</li>
<li>
<p>Les différentes méthodes de supervision de l&#8217;application: comment analyser les fichiers de logs et comment intercepter correctement une erreur si elle se présente et comment remonter l&#8217;information.</p>
</li>
<li>
<p>Une partie sur la sécurité et la sécurisation de l&#8217;hôte.</p>
</li>
</ul>
</div>
<div class="sect2">
<h3 id="_définition_de_linfrastructure">9.1. Définition de l&#8217;infrastructure</h3>
<div class="paragraph">
<p>Comme on l&#8217;a 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. On peut ainsi commencer petit, et suivre l&#8217;évolution des besoins en fonction de la charge estimée ou ressentie, ajouter un mécanisme de mise en cache, des logiciels de suivi, &#8230;&#8203;</p>
</div>
<div class="paragraph">
<p>Pour une mise ne production, le standard <strong>de facto</strong> est le suivant:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Nginx comme serveur principal</p>
</li>
<li>
<p>Gunicorn comme serveur d&#8217;application</p>
</li>
<li>
<p>Supervisorctl pour le monitoring</p>
</li>
<li>
<p>PostgreSQL comme base de données.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>C&#8217;est celle-ci que nous allons décrire ci-dessous.</p>
</div>
</div>
<div class="sect2">
<h3 id="_configuration_et_sécurisation_de_la_machine_hôte">9.2. Configuration et sécurisation de la machine hôte</h3>
<div class="paragraph">
<p>Supervisor, nginx, gunicorn, utilisateurs, groupes, &#8230;&#8203;</p>
</div>
<div class="imageblock">
<div class="content">
<img src="diag-9ca0787305b4c33019ebc11adc951857.png" alt="diag 9ca0787305b4c33019ebc11adc951857" width="310" height="157">
</div>
</div>
<div class="paragraph">
<p>Aussi : Docker, Heroku, Digital Ocean, Scaleway, OVH, &#8230;&#8203; Bref, sur Debian et CentOS pour avoir un panel assez large. On oublie Windows.</p>
</div>
</div>
<div class="sect2">
<h3 id="_mise_à_jour">9.3. Mise à jour</h3>
<div class="paragraph">
<p>Script de mise à jour.</p>
</div>
</div>
<div class="sect2">
<h3 id="_supervision">9.4. Supervision</h3>
<div class="paragraph">
<p>Qu&#8217;est-ce qu&#8217;on fait des logs après ? :-)</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_déploiement_sur_centos">10. Déploiement sur CentOS</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">yum update
groupadd --system webapps
groupadd --system gunicorn_sockets
useradd --system --gid webapps --shell /bin/bash --home /home/medplan medplan
mkdir -p /home/medplan
chown medplan:webapps /home/medplan</code></pre>
</div>
</div>
<div class="sect2">
<h3 id="_installation_des_dépendances_systèmes">10.1. Installation des dépendances systèmes</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">yum install python36 git tree -y
# CentOS 7 ne dispose que de la version 3.7 d'SQLite. On a besoin d'une version 3.8 au minimum:
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-devel-3.8.11-1.fc21.x86_64.rpm
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-3.8.11-1.fc21.x86_64.rpm
sudo yum install sqlite-3.8.11-1.fc21.x86_64.rpm sqlite-devel-3.8.11-1.fc21.x86_64.rpm -y</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_préparation_de_lenvironnement_utilisateur">10.2. Préparation de l&#8217;environnement utilisateur</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">su - medplan
cp /etc/skel/.bashrc .
cp /etc/skel/.bash_profile .
ssh-keygen
mkdir bin
mkdir .venvs
mkdir webapps
python3.6 -m venv .venvs/medplan
source .venvs/medplan/bin/activate
cd /home/medplan/webapps
git clone git@vmwmedtools:institutionnel/medplan.git</code></pre>
</div>
</div>
<div class="paragraph">
<p>La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.</p>
</div>
<div class="paragraph">
<p>A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes suivantes:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash"># en tant qu'utilisateur 'medplan'
source .venvs/medplan/bin/activate
pip install -U pip
pip install -r requirements/base.txt
pip install gunicorn
cd webapps/medplan
gunicorn config.wsgi:application --bind localhost:3000 --settings=config.settings_production</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_configuration_de_lapplication">10.3. Configuration de l&#8217;application</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">SECRET_KEY=&lt;set your secret key here&gt;
ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/medplan/static</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_création_des_répertoires_de_logs">10.4. Création des répertoires de logs</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-text" data-lang="text">mkdir -p /var/www/medplan/static</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_création_du_répertoire_pour_le_socket">10.5. Création du répertoire pour le socket</h3>
<div class="paragraph">
<p>Dans le fichier <code>/etc/tmpfiles.d/medplan.conf</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-text" data-lang="text">D /var/run/webapps 0775 medplan gunicorn_sockets -</code></pre>
</div>
</div>
<div class="paragraph">
<p>Suivi de la création par systemd :</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-text" data-lang="text">systemd-tmpfiles --create</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_gunicorn">10.6. Gunicorn</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">#!/bin/bash
# defines settings for gunicorn
NAME="Medplan"
DJANGODIR=/home/medplan/webapps/medplan
SOCKFILE=/var/run/webapps/gunicorn_medplan.sock
USER=medplan
GROUP=gunicorn_sockets
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production
DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`"
source /home/medplan/.venvs/medplan/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user $USER \
--bind=unix:$SOCKFILE \
--log-level=debug \
--log-file=-</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_supervision_2">10.7. Supervision</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">yum install supervisor -y</code></pre>
</div>
</div>
<div class="paragraph">
<p>On crée ensuite le fichier <code>/etc/supervisord.d/medplan.ini</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">[program:medplan]
command=/home/medplan/bin/start_gunicorn.sh
user=medplan
stdout_logfile=/var/log/medplan/medplan.log
autostart=true
autorestart=unexpected
redirect_stdout=true
redirect_stderr=true</code></pre>
</div>
</div>
<div class="paragraph">
<p>Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu&#8217;il tourne correctement:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">mkdir /var/log/medplan
chown medplan:nagios /var/log/medplan
systemctl enable supervisord
systemctl start supervisord.service
systemctl status supervisord.service
● supervisord.service - Process Monitoring and Control Daemon
Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago
Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code=exited, status=0/SUCCESS)
Main PID: 2310 (supervisord)
CGroup: /system.slice/supervisord.service
├─2310 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf
├─2313 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2317 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2318 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2321 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2322 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
└─2323 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
ls /var/run/webapps/</code></pre>
</div>
</div>
<div class="paragraph">
<p>On peut aussi vérifier que l&#8217;application est en train de tourner, à l&#8217;aide de la commande <code>supervisorctl</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$$$ supervisorctl status gwift
gwift RUNNING pid 31983, uptime 0:01:00</code></pre>
</div>
</div>
<div class="paragraph">
<p>Et pour gérer le démarrage ou l&#8217;arrêt, on peut passer par les commandes suivantes:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$$$ supervisorctl stop gwift
gwift: stopped
root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift
gwift: started
root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift
gwift: stopped
gwift: started</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_ouverture_des_ports">10.8. Ouverture des ports</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-text" data-lang="text">firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_installation_dnginx">10.9. Installation d&#8217;Nginx</h3>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code>yum install nginx -y
usermod -a -G gunicorn_sockets nginx</code></pre>
</div>
</div>
<div class="paragraph">
<p>On configure ensuite le fichier <code>/etc/nginx/conf.d/medplan.conf</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>upstream medplan_app {
server unix:/var/run/webapps/gunicorn_medplan.sock fail_timeout=0;
}
server {
listen 80;
server_name &lt;server_name&gt;;
root /var/www/medplan;
error_log /var/log/nginx/medplan_error.log;
access_log /var/log/nginx/medplan_access.log;
client_max_body_size 4G;
keepalive_timeout 5;
gzip on;
gzip_comp_level 7;
gzip_proxied any;
gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
location /static/ {
access_log off;
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://medplan_app;
}
}</pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_configuration_des_sauvegardes">10.10. Configuration des sauvegardes</h3>
<div class="paragraph">
<p>Les sauvegardes ont été configurées avec borg: <code>yum install borgbackup</code>.</p>
</div>
<div class="paragraph">
<p>C&#8217;est l&#8217;utilisateur medplan qui s&#8217;en occupe.</p>
</div>
<div class="listingblock">
<div class="content">
<pre>mkdir -p /home/medplan/borg-backups/
cd /home/medplan/borg-backups/
borg init medplan.borg -e=none
borg create medplan.borg::{now} ~/bin ~/webapps</pre>
</div>
</div>
<div class="paragraph">
<p>Et dans le fichier crontab :</p>
</div>
<div class="listingblock">
<div class="content">
<pre>0 23 * * * /home/medplan/bin/backup.sh</pre>
</div>
</div>
<div class="paragraph">
<p>On l&#8217;a déjà vu, Django se base sur un pattern type ActiveRecords pour l&#8217;ORM.</p>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
à vérifier ;-)
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>et supporte les principaux moteurs de bases de données connus: MariaDB (en natif depuis Django 3.0), PostgreSQL au travers de psycopg2 (en natif aussi), Microsoft SQLServer grâce aux drivers [&#8230;&#8203;à compléter] ou Oracle via <a href="https://oracle.github.io/python-cx_Oracle/">cx_Oracle</a>.</p>
</div>
<div class="admonitionblock warning">
<table>
<tr>
<td class="icon">
<div class="title">Warning</div>
</td>
<td class="content">
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. De fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause d&#8217;un Oracle 12.1).
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>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.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_postgresql">11. PostgreSQL</h2>
<div class="sectionbody">
<div class="paragraph">
<p>On commence par installer PostgreSQL.</p>
</div>
<div class="paragraph">
<p>Par exemple, dans le cas de debian, on exécute la commande suivante:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$$$ aptitude install postgresql postgresql-contrib</code></pre>
</div>
</div>
<div class="paragraph">
<p>Ensuite, on crée un utilisateur pour la DB:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$$$ su - postgres
postgres@gwift:~$ createuser --interactive -P
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
postgres@gwift:~$</code></pre>
</div>
</div>
<div class="paragraph">
<p>Finalement, on peut créer la DB:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">postgres@gwift:~$ createdb --owner gwift_user gwift
postgres@gwift:~$ exit
logout
$$$</code></pre>
</div>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
penser à inclure un bidule pour les backups.
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_mariadb">12. MariaDB</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Idem, installation, configuration, backup, tout ça.
A copier de grimboite, je suis sûr d&#8217;avoir des notes là-dessus.</p>
</div>
</div>
</div>
<h1 id="_modélisation" class="sect0">Modélisation</h1>
<div class="openblock partintro">
<div class="content">
Dans ce chapitre, on va parler de plusieurs concepts utiles au développement rapide d&#8217;une application. On parlera de modélisation, de migrations, d&#8217;administration auto-générée.
</div>
</div>
<div class="sect1">
<h2 id="_modélisation_2">13. Modélisation</h2>
<div class="sectionbody">
<div class="paragraph">
<p>On va aborder la modélisation des objets en elle-même, qui s&#8217;apparente à la conception de la base de données.</p>
</div>
<div class="paragraph">
<p>Django utilise un modèle <a href="https://fr.wikipedia.org/wiki/Mapping_objet-relationnel">ORM</a> - c&#8217;est-à-dire que chaque objet peut s&#8217;apparenter à une table SQL, mais en ajoutant une couche propre au paradigme orienté objet. Il sera ainsi possible de définir facilement des notions d&#8217;héritage (tout en restant dans une forme d&#8217;héritage simple), la possibilité d&#8217;utiliser des propriétés spécifiques, des classes intermédiaires, &#8230;&#8203;</p>
</div>
<div class="paragraph">
<p>L&#8217;avantage de tout ceci est que tout reste au niveau du code. Si l&#8217;on revient sur la méthodologie des douze facteurs, ce point concerne principalement la minimisation de la divergence entre les environnements d&#8217;exécution. Déployer une nouvelle instance de l&#8217;application pourra être réalisé directement à partir d&#8217;une seule et même commande, dans la mesure où <strong>tout est embarqué au niveau du code</strong>.</p>
</div>
<div class="paragraph">
<p>Assez de blabla, on démarre !</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_queryset_managers">14. Queryset &amp; managers</h2>
<div class="sectionbody">
<div class="paragraph">
<p>L&#8217;ORM de Django propose par défaut deux objets hyper importants:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Les managers, qui consistent en un point d&#8217;entrée pour accéder aux objets persistants</p>
</li>
<li>
<p>Les querysets, 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.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>En plus de cela, il faut bien tenir compte des propriétés <code>Meta</code> 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.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_forms">15. Forms</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Ou comment valider proprement des données entrantes.</p>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
intégrer le dessin XKCD avec Little Bobby Table sur l&#8217;assainissement des données en entrée :-p
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>Quand on parle de <code>forms</code>, 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 <code>forms</code> 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.</p>
</div>
<div class="paragraph">
<p>L&#8217;exemple le plus simple est un fichier <code>.csv</code>: 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.</p>
</div>
<div class="paragraph">
<p>Mauvaise idée.</p>
</div>
<div class="paragraph">
<p>Les données fournies par un utilisateur <strong>doivent</strong> <strong>toujours</strong> ê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.</p>
</div>
<div class="paragraph">
<p>Le flux à suivre est le suivant:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Création d&#8217;une instance grâce à un dictionnaire</p>
</li>
<li>
<p>Validation des données et des informations reçues</p>
</li>
<li>
<p>Traitement, si la validation a réussi.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>Ils jouent également plusieurs rôles:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Validation des données, en plus de celles déjà définies au niveau du modèle</p>
</li>
<li>
<p>Contrôle sur le rendu à appliquer aux champs</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>Ils agissent come une glue entre l&#8217;utilisateur et la modélisation de vos structures de données.</p>
</div>
<div class="sect2">
<h3 id="_dépendance_avec_le_modèle">15.1. Dépendance avec le modèle</h3>
<div class="paragraph">
<p>Un <strong>form</strong> peut dépendre d&#8217;une autre classe Django. Pour cela, il suffit de fixer l&#8217;attribut <code>model</code> au niveau de la <code>class Meta</code> dans la définition.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">from django import forms
from wish.models import Wishlist
class WishlistCreateForm(forms.ModelForm):
class Meta:
model = Wishlist
fields = ('name', 'description')</code></pre>
</div>
</div>
<div class="paragraph">
<p>De cette manière, notre form dépendra automatiquement des champs déjà déclarés dans la classe <code>Wishlist</code>. Cela suit le principe de <code>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</code>, nous pourrons nous assurer de faire évoluer le formulaire en fonction du modèle sur lequel il se base.</p>
</div>
</div>
<div class="sect2">
<h3 id="_rendu_et_affichage">15.2. Rendu et affichage</h3>
<div class="paragraph">
<p>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 <code>Meta</code>. Sinon, ils peuvent l&#8217;être directement au niveau du champ.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">from django import forms
from datetime import date
from .models import Accident
class AccidentForm(forms.ModelForm):
class Meta:
model = Accident
fields = ('gymnast', 'educative', 'date', 'information')
widgets = {
'date' : forms.TextInput(
attrs={
'class' : 'form-control',
'data-provide' : 'datepicker',
'data-date-format' : 'dd/mm/yyyy',
'placeholder' : date.today().strftime("%d/%m/%Y")
}),
'information' : forms.Textarea(
attrs={
'class' : 'form-control',
'placeholder' : 'Context (why, where, ...)'
})</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_squelette_par_défaut">15.3. Squelette par défaut</h3>
<div class="paragraph">
<p>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.</p>
</div>
</div>
<div class="sect2">
<h3 id="_crispy_forms">15.4. Crispy-forms</h3>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>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 `Django-Crispy-Forms &lt;<a href="http://django-crispy-forms.readthedocs.io/en/latest/&gt;`_" class="bare">http://django-crispy-forms.readthedocs.io/en/latest/&gt;`_</a>. Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation et uni-form) et permet de contrôler entièrement le <strong>layout</strong> et la présentation.</p>
</div>
<div class="paragraph">
<p>(c/c depuis le lien ci-dessous)</p>
</div>
<div class="paragraph">
<p>Pour chaque champ, crispy-forms va :</p>
</div>
<div class="ulist">
<ul>
<li>
<p>utiliser le <code>verbose_name</code> comme label.</p>
</li>
<li>
<p>vérifier les paramètres <code>blank</code> et <code>null</code> pour savoir si le champ est obligatoire.</p>
</li>
<li>
<p>utiliser le type de champ pour définir le type de la balise <code>&lt;input&gt;</code>.</p>
</li>
<li>
<p>récupérer les valeurs du paramètre <code>choices</code> (si présent) pour la balise <code>&lt;select&gt;</code>.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p><a href="http://dotmobo.github.io/django-crispy-forms.html" class="bare">http://dotmobo.github.io/django-crispy-forms.html</a></p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_validation_des_données">16. Validation des données</h2>
<div class="sectionbody">
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
parler ici des méthodes <code>clean</code>.
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_en_conclusion">17. En conclusion</h2>
<div class="sectionbody">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Toute donnée entrée par l&#8217;utilisateur <strong>doit</strong> passer par une instance de <code>form</code>.</p>
</li>
<li>
<p>euh ?</p>
</li>
</ol>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_migrations">18. Migrations</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Les migrations (comprendre les "migrations du schéma de base de données") 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.</p>
</div>
<div class="paragraph">
<p>Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. Le développeur s&#8217;occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, <em>squashées</em>, &#8230;&#8203; et feront partie intégrante du processus de mise à jour de l&#8217;application.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_modèle_vue_template">19. Modèle-vue-template</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Dans un <strong>pattern</strong> MVC classique, la traduction immédiate du <strong>contrôleur</strong> est une <strong>vue</strong>. Et comme on le verra par la suite, la <strong>vue</strong> est en fait le <strong>template</strong>.
Les vues agrègent donc les informations à partir d&#8217;un des composants et les font transiter vers un autre. 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.</p>
</div>
<div class="sect2">
<h3 id="_vues">19.1. Vues</h3>
<div class="paragraph">
<p>Une vue correspond à un contrôleur dans le pattern MVC. Tout ce que vous pourrez définir au niveau du fichier <code>views.py</code> fera le lien entre le modèle stocké dans la base de données et ce avec quoi l&#8217;utilisateur pourra réellement interagir (le <code>template</code>).</p>
</div>
<div class="paragraph">
<p>Chaque vue peut etre représentée de deux manières: soit par des fonctions, soit par des classes. Le comportement leur est propre, mais le résultat reste identique. Le lien entre l&#8217;URL à laquelle l&#8217;utilisateur accède et son exécution est faite au travers du fichier <code>gwift/urls.py</code>, comme on le verra par la suite.</p>
</div>
<div class="sect3">
<h4 id="_function_based_views">19.1.1. Function Based Views</h4>
<div class="paragraph">
<p>Les fonctions (ou <code>FBV</code> pour <strong>Function Based Views</strong>) permettent une implémentation classique des contrôleurs. Au fur et à mesure de votre implémentation, on se rendra compte qu&#8217;il y a beaucoup de répétitions dans ce type d&#8217;implémentation: elles ne sont pas obsolètes, mais dans certains cas, il sera préférable de passer par les classes.</p>
</div>
<div class="paragraph">
<p>Pour définir la liste des <code>WishLists</code> actuellement disponibles, on précédera de la manière suivante:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Définition d&#8217;une fonction qui va récupérer les objets de type <code>WishList</code> dans notre base de données. La valeur de retour sera la construction d&#8217;un dictionnaire (le <strong>contexte</strong>) qui sera passé à un template HTML. On démandera à ce template d&#8217;effectuer le rendu au travers de la fonction <code>render</code>, qui est importée par défaut dans le fichier <code>views.py</code>.</p>
</li>
<li>
<p>Construction d&#8217;une URL qui permettra de lier l&#8217;adresse à l&#8217;exécution de la fonction.</p>
</li>
<li>
<p>Définition du squelette.</p>
</li>
</ol>
</div>
<div class="sect4">
<h5 id="_définition_de_la_fonction">Définition de la fonction</h5>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># wish/views.py
from django.shortcuts import render
from .models import Wishlist
def wishlists(request):
w = Wishlist.objects.all()
return render(request, 'wish/list.html',{ 'wishlists': w })</code></pre>
</div>
</div>
</div>
<div class="sect4">
<h5 id="_construction_de_lurl">Construction de l&#8217;URL</h5>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from wish import views as wish_views
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', wish_views.wishlists, name='wishlists'),
]</code></pre>
</div>
</div>
</div>
<div class="sect4">
<h5 id="_définition_du_squelette">Définition du squelette</h5>
<div class="paragraph">
<p>A ce stade, vérifiez que la variable <code>TEMPLATES</code> est correctement initialisée dans le fichier <code>gwift/settings.py</code> et que le fichier <code>templates/wish/list.html</code> ressemble à ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-jinj2" data-lang="jinj2">&lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta http-equiv="x-ua-compatible" content="ie=edge"&gt;
&lt;title&gt;&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;Mes listes de souhaits&lt;/p&gt;
&lt;ul&gt;
{% for wishlist in wishlists %}
&lt;li&gt;{{ wishlist.name }}: {{ wishlist.description }}&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</div>
</div>
</div>
<div class="sect4">
<h5 id="_exécution">Exécution</h5>
<div class="paragraph">
<p>A présent, ajoutez quelques listes de souhaits grâce à un <strong>shell</strong>, puis lancez le serveur:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ python manage.py shell
&gt;&gt;&gt; from wish.models import Wishlist
&gt;&gt;&gt; Wishlist.create('Décembre', "Ma liste pour les fêtes de fin d'année")
&lt;Wishlist: Wishlist object&gt;
&gt;&gt;&gt; Wishlist.create('Anniv 30 ans', "Je suis vieux! Faites des dons!")
&lt;Wishlist: Wishlist object&gt;</code></pre>
</div>
</div>
<div class="paragraph">
<p>Lancez le serveur grâce à la commande <code>python manage.py runserver</code>, ouvrez un navigateur quelconque et rendez-vous à l&#8217;adresse `http://localhost:8000 &lt;<a href="http://localhost:8000&gt;`_" class="bare">http://localhost:8000&gt;`_</a>. Vous devriez obtenir le résultat suivant:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>image:: mvc/my-first-wishlists.png
:align: center</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>Rien de très sexy, aucune interaction avec l&#8217;utilisateur, très peu d&#8217;utilisation des variables contextuelles, mais c&#8217;est un bon début! =)</p>
</div>
</div>
</div>
<div class="sect3">
<h4 id="_class_based_views">19.1.2. Class Based Views</h4>
<div class="paragraph">
<p>Les classes, de leur côté, implémente le <strong>pattern</strong> objet et permettent d&#8217;arriver facilement à un résultat en très peu de temps, parfois même en définissant simplement quelques attributs, et rien d&#8217;autre. Pour l&#8217;exemple, on va définir deux classes qui donnent exactement le même résultat que la fonction <code>wishlists</code> ci-dessus. Une première fois en utilisant une classe générique vierge, et ensuite en utilisant une classe de type <code>ListView</code>.</p>
</div>
<div class="sect4">
<h5 id="_classe_générique">Classe générique</h5>
<div class="paragraph">
<p>blah</p>
</div>
</div>
<div class="sect4">
<h5 id="_listview">ListView</h5>
<div class="paragraph">
<p>Les classes génériques implémentent un aspect bien particulier de la représentation d&#8217;un modèle, en utilisant très peu d&#8217;attributs. Les principales classes génériques sont de type <code>ListView</code>, [&#8230;&#8203;]. L&#8217;implémentation consiste, exactement comme pour les fonctions, à:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Définir la classe</p>
</li>
<li>
<p>Créer l&#8217;URL</p>
</li>
<li>
<p>Définir le squelette.</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># wish/views.py
from django.views.generic import ListView
from .models import Wishlist
class WishListList(ListView):
context_object_name = 'wishlists'
model = Wishlist
template_name = 'wish/list.html'</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]</code></pre>
</div>
</div>
<div class="paragraph">
<p>C&#8217;est tout. Lancez le serveur, le résultat sera identique. Par inférence, Django construit beaucoup d&#8217;informations: si on n&#8217;avait pas spécifié les variables <code>context_object_name</code> et <code>template_name</code>, celles-ci auraient pris les valeurs suivantes:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>context_object_name</code>: <code>wishlist_list</code> (ou plus précisément, le nom du modèle suivi de <code>_list</code>)</p>
</li>
<li>
<p><code>template_name</code>: <code>wish/wishlist_list.html</code> (à nouveau, le fichier généré est préfixé du nom du modèle).
=== Templates</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Avant de commencer à interagir avec nos données au travers de listes, formulaires et IHM sophistiquées, quelques mots sur les templates: il s&#8217;agit en fait de <strong>squelettes</strong> de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher dans le format que vous définirez. En intégrant un ensemble de <strong>tags</strong>, cela vous permettra de greffer les données reçues en entrée dans un patron prédéfini.</p>
</div>
<div class="paragraph">
<p>Une page HTML basique ressemble à ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-html" data-lang="html">&lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta http-equiv="x-ua-compatible" content="ie=edge"&gt;
&lt;title&gt;&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;Hello world!&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</div>
</div>
<div class="paragraph">
<p>Notre première vue permettra de récupérer la liste des objets de type <code>Wishlist</code> que nous avons définis dans le fichier <code>wish/models.py</code>. Supposez que cette liste soit accessible <strong>via</strong> la clé <code>wishlists</code> d&#8217;un dictionnaire passé au template. Elle devient dès lors accessible grâce aux tags <code>{% for wishlist in wishlists %}</code>. A chaque tour de boucle, on pourra directement accéder à la variable <code>{{ wishlist }}</code>. De même, il sera possible d&#8217;accéder aux propriétés de cette objet de la même manière: <code>{{ wishlist.id }}</code>, <code>{{ wishlist.description }}</code>, &#8230;&#8203; et d&#8217;ainsi respecter la mise en page que nous souhaitons.</p>
</div>
<div class="paragraph">
<p>En reprenant l&#8217;exemple de la page HTML définie ci-dessus, on pourra l&#8217;agrémenter de la manière suivante:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-django" data-lang="django">&lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta http-equiv="x-ua-compatible" content="ie=edge"&gt;
&lt;title&gt;&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;Mes listes de souhaits&lt;/p&gt;
&lt;ul&gt;
{% for wishlist in wishlists %}
&lt;li&gt;{{ wishlist.name }}: {{ wishlist.description }}&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</div>
</div>
<div class="paragraph">
<p>Vous pouvez déjà copier ce contenu dans un fichier <code>templates/wsh/list.html</code>, on en aura besoin par la suite.</p>
</div>
</div>
</div>
<div class="sect3">
<h4 id="_structure_et_configuration">19.1.3. Structure et configuration</h4>
<div class="paragraph">
<p>Il est conseillé que les templates respectent la structure de vos différentes applications, mais dans un répertoire à part. Par convention, nous les placerons dans un répertoire <code>templates</code>. La hiérarchie des fichiers devient alors celle-ci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">$ tree templates/
templates/
└── wish
└── list.html</code></pre>
</div>
</div>
<div class="paragraph">
<p>Par défaut, Django cherchera les templates dans les répertoirer d&#8217;installation. Vous devrez vous éditer le fichier <code>gwift/settings.py</code> et ajouter, dans la variable <code>TEMPLATES</code>, la clé <code>DIRS</code> de la manière suivante:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">TEMPLATES = [
{
...
'DIRS': [ 'templates' ],
...
},
]</code></pre>
</div>
</div>
</div>
<div class="sect3">
<h4 id="_builtins">19.1.4. Builtins</h4>
<div class="paragraph">
<p>Django vient avec un ensemble de <strong>tags</strong>. On a vu la boucle <code>for</code> ci-dessus, mais il existe <a href="https://docs.djangoproject.com/fr/1.9/ref/templates/builtins/">beaucoup d&#8217;autres tags nativement présents</a>. Les principaux sont par exemple:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>{% if &#8230;&#8203; %} &#8230;&#8203; {% elif &#8230;&#8203; %} &#8230;&#8203; {% else %} &#8230;&#8203; {% endif %}</code>: permet de vérifier une condition et de n&#8217;afficher le contenu du bloc que si la condition est vérifiée.</p>
</li>
<li>
<p>Opérateurs de comparaisons: <code>&lt;</code>, <code>&gt;</code>, <code>==</code>, <code>in</code>, <code>not in</code>.</p>
</li>
<li>
<p>Regroupements avec le tag <code>{% regroup &#8230;&#8203; by &#8230;&#8203; as &#8230;&#8203; %}</code>.</p>
</li>
<li>
<p><code>{% url %}</code> pour construire facilement une URL</p>
</li>
<li>
<p>&#8230;&#8203;</p>
</li>
</ul>
</div>
</div>
<div class="sect3">
<h4 id="_non_builtins">19.1.5. Non-builtins</h4>
<div class="paragraph">
<p>En plus des quelques tags survolés ci-dessus, il est également possible de construire ses propres tags. La structure est un peu bizarre, car elle consiste à ajouter un paquet dans une de vos applications, à y définir un nouveau module et à y définir un ensemble de fonctions. Chacune de ces fonctions correspondra à un tag appelable depuis vos templates.</p>
</div>
<div class="paragraph">
<p>Il existe trois types de tags <strong>non-builtins</strong>:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Les filtres - on peut les appeler grâce au <strong>pipe</strong> <code>|</code> directement après une valeur dans le template.</p>
</li>
<li>
<p>Les tags simples - ils peuvent prendre une valeur ou plusieurs en paramètre et retourne une nouvelle valeur. Pour les appeler, c&#8217;est <strong>via</strong> les tags <code>{% nom_de_la_fonction param1 param2 &#8230;&#8203; %}</code>.</p>
</li>
<li>
<p>Les tags d&#8217;inclusion: ils retournent un contexte (ie. un dictionnaire), qui est ensuite passé à un nouveau template.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>Pour l&#8217;implémentation:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>On prend l&#8217;application <code>wish</code> et on y ajoute un répertoire <code>templatetags</code>, ainsi qu&#8217;un fichier <code><em>init</em>.py</code>.</p>
</li>
<li>
<p>Dans ce nouveau paquet, on ajoute un nouveau module que l&#8217;on va appeler <code>tools.py</code></p>
</li>
<li>
<p>Dans ce module, pour avoir un aperçu des possibilités, on va définir trois fonctions (une pour chaque type de tags possible).</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">[Inclure un tree du dossier template tags]</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># wish/tools.py
from django import template
from wish.models import Wishlist
register = template.Library()
@register.filter(is_safe=True)
def add_xx(value):
return '%sxx' % value
@register.simple_tag
def current_time(format_string):
return datetime.datetime.now().strftime(format_string)
@register.inclusion_tag('wish/templatetags/wishlists_list.html')
def wishlists_list():
return { 'list': Wishlist.objects.all() }</code></pre>
</div>
</div>
<div class="paragraph">
<p>Pour plus d&#8217;informations, la <a href="https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#writing-custom-template-tags">documentation officielle est un bon début</a>.
=== Mise en page</p>
</div>
<div class="paragraph">
<p>Pour que nos pages soient un peu plus <strong>eye-candy</strong> que ce qu&#8217;on a présenté ci-dessus, nous allons modifié notre squelette pour qu&#8217;il se base sur <code>Bootstrap &lt;<a href="http://getbootstrap.com/&gt;`_" class="bare">http://getbootstrap.com/&gt;`_</a>. Nous placerons une barre de navigation principale, la possibilité de se connecter pour l&#8217;utilisateur et définirons quelques emplacements à utiliser par la suite. Reprenez votre fichier `base.html</code> et modifiez le comme ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-html" data-lang="html">{% load staticfiles %}
&lt;!DOCTYPE html&gt;
&lt;!--[if IE 9]&gt;&lt;html class="lt-ie10" lang="en" &gt; &lt;![endif]--&gt;
&lt;html class="no-js" lang="en"&gt;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
&lt;link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"&gt;
&lt;script src="//code.jquery.com/jquery.min.js"&gt;&lt;/script&gt;
&lt;script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"&gt;&lt;/script&gt;
&lt;link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'&gt;
&lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css"&gt;
&lt;link href="{% static 'css/style.css' %}" rel="stylesheet"&gt;
&lt;link rel="icon" href="{% static 'img/favicon.ico' %}" /&gt;
&lt;title&gt;Gwift&lt;/title&gt;
&lt;/head&gt;
&lt;body class="base-body"&gt;
&lt;!-- navigation --&gt;
&lt;div class="nav-wrapper"&gt;
&lt;div id="nav"&gt;
&lt;nav class="navbar navbar-default navbar-static-top navbar-shadow"&gt;
&lt;div class="container-fluid"&gt;
&lt;div class="navbar-header"&gt;
&lt;button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#menuNavbar"&gt;
&lt;span class="icon-bar"&gt;&lt;/span&gt;
&lt;span class="icon-bar"&gt;&lt;/span&gt;
&lt;span class="icon-bar"&gt;&lt;/span&gt;
&lt;/button&gt;
&lt;a class="navbar-brand" href="/"&gt;
&lt;img src="{% static 'img/gwift-20x20.png' %}" /&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div class="collapse navbar-collapse" id="menuNavbar"&gt;
{% include "_menu_items.html" %}
&lt;/div&gt;
&lt;/div&gt;
&lt;/nav&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- end navigation --&gt;
&lt;!-- content --&gt;
&lt;div class="container"&gt;
&lt;div class="row"&gt;
&lt;div class="col-md-8"&gt;
{% block content %}{% endblock %}
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- end content --&gt;
&lt;!-- footer --&gt;
&lt;footer class="footer"&gt;
{% include "_footer.html" %}
&lt;/footer&gt;
&lt;!-- end footer --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</div>
</div>
<div class="paragraph">
<p>Quelques remarques:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>La première ligne du fichier inclut le <strong>tag</strong> <code>{% load staticfiles %}</code>. On y reviendra par la suite, mais en gros, cela permet de faciliter la gestion des fichiers statiques, notamment en les appelent grâce à la commande <code>{% static 'img/header.png' %}</code> ou <code>{% static 'css/app_style.css' %}</code>.</p>
</li>
<li>
<p>La balise <code>&lt;head /&gt;</code> est bourée d&#8217;appel vers des ressources stockées sur des :abbr:`CDN (Content Delivery Networks)`.</p>
</li>
<li>
<p>Les balises <code>{% block content %} {% endblock %}</code> permettent de faire hériter du contenu depuis une autre page. On l&#8217;utilise notamment dans notre page <code>templates/wish/list.html</code>.</p>
</li>
<li>
<p>Pour l&#8217;entête et le bas de page, on fait appel aux balises <code>{% include 'nom_du_fichier.html' %}</code>: ces fichiers sont des fichiers physiques, placés sur le filesystem, juste à côté du fichier <code>base.html</code>. De façon bête et méchante, cela inclut juste du contenu HTML. Le contenu des fichiers <code>_menu_items.html</code> et <code>_footer.html</code> est copié ci-dessous.</p>
</li>
</ul>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-html" data-lang="html">&lt;!-- gwift/templates/wish/list.html --&gt;
{% extends "base.html" %}
{% block content %}
&lt;p&gt;Mes listes de souhaits&lt;/p&gt;
&lt;ul&gt;
{% for wishlist in wishlists %}
&lt;li&gt;{{ wishlist.name }}: {{ wishlist.description }}&lt;/li&gt;
{% endfor %}
&lt;/ul&gt;
{% endblock %}</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-html" data-lang="html">&lt;!-- gwift/templates/_menu_items.html --&gt;
&lt;ul class="nav navbar-nav"&gt;
&lt;li class=""&gt;
&lt;a href="#"&gt;
&lt;i class="fa fa-calendar"&gt;&lt;/i&gt; Mes listes
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul class="nav navbar-nav navbar-right"&gt;
&lt;li class=""&gt;
&lt;a href="#"&gt;
&lt;i class="fa fa-user"&gt;&lt;/i&gt; Login / Register
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-html" data-lang="html">&lt;!-- gwift/templates/_footer.html --&gt;
&lt;div class="container"&gt;
Copylefted '16
&lt;/div&gt;</code></pre>
</div>
</div>
<div class="paragraph">
<p>En fonction de vos affinités, vous pourriez également passer par `PluCSS &lt;<a href="http://plucss.pluxml.org/&gt;`" class="bare">http://plucss.pluxml.org/&gt;`</a><em>, `Pure &lt;<a href="http://purecss.io/&gt;`" class="bare">http://purecss.io/&gt;`</a></em>, `Knacss &lt;<a href="http://knacss.com/&gt;`" class="bare">http://knacss.com/&gt;`</a><em>, `Cascade &lt;<a href="http://www.cascade-framework.com/&gt;`" class="bare">http://www.cascade-framework.com/&gt;`</a></em>, `Semantic &lt;<a href="http://semantic-ui.com/&gt;`_" class="bare">http://semantic-ui.com/&gt;`_</a> ou `Skeleton &lt;<a href="http://getskeleton.com/&gt;`_" class="bare">http://getskeleton.com/&gt;`_</a>. Pour notre plus grand bonheur, les frameworks de ce type pullulent. Reste à choisir le bon.</p>
</div>
<div class="paragraph">
<p><strong>A priori</strong>, si vous relancez le serveur de développement maintenant, vous devriez déjà voir les modifications&#8230;&#8203; Mais pas les images, ni tout autre fichier statique.</p>
</div>
</div>
<div class="sect3">
<h4 id="_fichiers_statiques">19.1.6. Fichiers statiques</h4>
<div class="paragraph">
<p>Si vous ouvrez la page et que vous lancez la console de développement (F12, sur la majorité des navigateurs), vous vous rendrez compte que certains fichiers ne sont pas disponibles. Il s&#8217;agit des fichiers suivants:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>/static/css/style.css</code></p>
</li>
<li>
<p><code>/static/img/favicon.ico</code></p>
</li>
<li>
<p><code>/static/img/gwift-20x20.png</code>.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>En fait, par défaut, les fichiers statiques sont récupérés grâce à deux handlers:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p><code>django.contrib.staticfiles.finders.FileSystemFinder</code> et . <code>django.contrib.staticfiles.finders.AppDirectoriesFinder</code>.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>En fait, Django va considérer un répertoire <code>static</code> à l&#8217;intérieur de chaque application. Si deux fichiers portent le même nom, le premier trouvé sera pris. Par facilité, et pour notre développement, nous placerons les fichiers statiques dans le répertoire <code>gwift/static</code>. On y trouve donc:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-bash" data-lang="bash">[inclure un tree du répertoire gwift/static]</code></pre>
</div>
</div>
<div class="paragraph">
<p>Pour indiquer à Django que vous souhaitez aller y chercher vos fichiers, il faut initialiser la <code>variable &lt;<a href="https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_DIRS&gt;`_" class="bare">https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_DIRS&gt;`_</a> `STATICFILES_DIRS</code> dans le fichier <code>settings/base.py</code>. Vérifiez également que la variable <code>STATIC_URL</code> est correctement définie.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># gwift/settings/base.py
STATIC_URL = '/static/'</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># gwift/settings/dev.py
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]</code></pre>
</div>
</div>
<div class="paragraph">
<p>En production par contre, nous ferons en sorte que le contenu statique soit pris en charge par le front-end Web (Nginx), raison pour laquelle cette variable n&#8217;est initialisée que dans le fichier des paramètres liés au développement.</p>
</div>
<div class="paragraph">
<p>Au final, cela ressemble à ceci:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>image:: mvc/my-first-wishlists.png
:align: center
=== URLs</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>La gestion des URLs permet <strong>grosso modo</strong> d&#8217;assigner une adresse paramétrée ou non à une fonction Python. La manière simple consiste à modifier le fichier <code>gwift/settings.py</code> pour y ajouter nos correspondances. Par défaut, le fichier ressemble à ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]</code></pre>
</div>
</div>
<div class="paragraph">
<p>Le champ <code>urlpatterns</code> associe un ensemble d&#8217;adresses à des fonctions. Dans le fichier <strong>nu</strong>, seul le <strong>pattern</strong> <code>admin`_ est défini, et inclut toutes les adresses qui sont définies dans le fichier `admin.site.urls</code>. Reportez-vous à l&#8217;installation de l&#8217;environnement: ce fichier contient les informations suivantes:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>_`admin`: Rappelez-vous de vos expressions régulières: <code>^</code> indique le début de la chaîne.</p>
</li>
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre># admin.site.urls.py</pre>
</div>
</div>
</li>
</ol>
</div>
</div>
<div class="sect3">
<h4 id="_reverse">19.1.7. Reverse</h4>
<div class="paragraph">
<p>En associant un nom ou un libellé à chaque URL, il est possible de récupérer sa <strong>traduction</strong>. Cela implique par contre de ne plus toucher à ce libellé par la suite&#8230;&#8203;</p>
</div>
<div class="paragraph">
<p>Dans le fichier <code>urls.py</code>, on associe le libellé <code>wishlists</code> à l&#8217;URL <code>r'^$</code> (c&#8217;est-à-dire la racine du site):</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]</code></pre>
</div>
</div>
<div class="paragraph">
<p>De cette manière, dans nos templates, on peut à présent construire un lien vers la racine avec le tags suivant:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-html" data-lang="html">&lt;a href="{% url 'wishlists' %}"&gt;{{ yearvar }} Archive&lt;/a&gt;</code></pre>
</div>
</div>
<div class="paragraph">
<p>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:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">from django.core.urlresolvers import reverse_lazy
wishlists_url = reverse_lazy('wishlists')</code></pre>
</div>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<div class="title">Note</div>
</td>
<td class="content">
Ne pas oublier de parler des sessions. Mais je ne sais pas si c&#8217;est le bon endroit.
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_logging">20. Logging</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Si on veut propager les logs entre applications, il faut bien spécifier l&#8217;attribut <code>propagate</code>, sans quoi on s&#8217;arrêtera au module sans prendre en considération les sous-modules.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': os.path.join(SRC_DIR, 'log', 'log.txt'),
},
},
'loggers': {
'mv': {
'handlers': ['file'],
'level': 'DEBUG',
'propagate': True,
},
},
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Par exemple:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">'loggers': {
'mv': { # Parent
'handlers': ['file'],
'level': 'DEBUG',
},
'mv.models': { # Enfant
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
}
},</code></pre>
</div>
</div>
<div class="paragraph">
<p>Et dans le fichier <code>mv/models.py</code>, on a ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">logger = logging.getLogger(__name__)
logger.debug('helloworld');</code></pre>
</div>
</div>
<div class="paragraph">
<p>Le log sera écrit dans la console <strong>ET</strong> dans le fichier.</p>
</div>
<div class="paragraph">
<p>Par contre, si on retire l&#8217;attribut <code>propagate: True</code> (ou qu&#8217;on le change en <code>propagate: False</code>), le même code ci-dessus n&#8217;écrit que dans la console. Simplement parce que le log associé à un package considère par défaut ses enfants, alors que le log associé à un module pas. [Par exemple](<a href="https://docs.djangoproject.com/en/2.1/topics/logging/#examples" class="bare">https://docs.djangoproject.com/en/2.1/topics/logging/#examples</a>).</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_administration">21. Administration</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Woké. On va commencer par la <strong>partie à ne <em>surtout</em> (<em>surtout</em> !!) pas faire en premier dans un projet Django</strong>. 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.</p>
</div>
<div class="paragraph">
<p>C&#8217;est faux.</p>
</div>
<div class="paragraph">
<p>L&#8217;administration est une sorte de tour de contrôle évoluée; elle se base sur le modèle de données programmé et construit dynamiquement les formulaires qui lui est associé. Elle joue avec les clés primaires, étrangères, les champs et types de champs par <a href="https://fr.wikipedia.org/wiki/Introspection">introspection</a>.</p>
</div>
<div class="paragraph">
<p>Son problème est qu&#8217;elle présente une courbe d&#8217;apprentissage asymptotique. Il est <strong>très</strong> 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.</p>
</div>
<div class="paragraph">
<p>Elle 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.</p>
</div>
<div class="paragraph">
<p>Une bonne idée consiste à développer l&#8217;administration dans un premier temps, en <strong>gardant en tête qu&#8217;il sera nécessaire de développer des concepts spécifiques</strong>. 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. C&#8217;est un excellent outil de prototypage et de preuve de concept.</p>
</div>
<div class="sect2">
<h3 id="_quelques_conseils">21.1. Quelques conseils</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Surchargez la méthode <code><em>str</em>(self)</code> pour chaque classe que vous aurez définie dans le modèle. Cela permettra de construire une représentation textuelle qui représentera l&#8217;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).</p>
</li>
<li>
<p>La méthode <code>get_absolute_url(self)</code> retourne l&#8217;URL à laquelle on peut accéder pour obtenir les détails d&#8217;une instance. Par exemple:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">def get_absolute_url(self):
return reverse('myapp.views.details', args=[self.id])</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Les attributs <code>Meta</code>:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">class Meta:
ordering = ['-field1', 'field2']
verbose_name = 'my class in singular'
verbose_name_plural = 'my class when is in a list!'</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Le titre:</p>
<div class="ulist">
<ul>
<li>
<p>Soit en modifiant le template de l&#8217;administration</p>
</li>
<li>
<p>Soit en ajoutant l&#8217;assignation suivante dans le fichier <code>urls.py</code>: <code>admin.site.site_header = "SuperBook Secret Area</code>.</p>
</li>
</ul>
</div>
</li>
<li>
<p>Prefetch</p>
</li>
</ol>
</div>
<div class="paragraph">
<p><a href="https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64" class="bare">https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64</a></p>
</div>
<div class="paragraph">
<p><a href="https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614" class="bare">https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614</a></p>
</div>
<div class="paragraph">
<p>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é <code>list_select_related</code> de la classe d&#8217;Admin, afin d&#8217;appliquer une jointure par défaut et
et gagner en performances.</p>
</div>
</div>
</div>
</div>
<h1 id="_go_live" class="sect0">Go Live !</h1>
<div class="openblock partintro">
<div class="content">
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>Pour prendre un exemple concret, nous allons créer un site permettant de gérer des listes de souhaits, que nous appellerons <code>gwift</code> (pour <code>GiFTs and WIshlisTs</code> :)).</p>
</div>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>Ensuite, nous pourrons traduire ces besoins en fonctionnalités et finalement effectuer le développement</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_besoins_utilisateurs">22. Besoins utilisateurs</h2>
<div class="sectionbody">
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>Il sera nécessaire de s&#8217;authentifier pour :</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Créer une liste associée à l&#8217;utilisateur en cours</p>
</li>
<li>
<p>Ajouter un nouvel élément à une liste</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Il ne sera pas nécessaire de s&#8217;authentifier pour :</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Faire une promesse d&#8217;offre pour un élément appartenant à une liste, associée à un utilisateur.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>Un souhait pourrait aussi être réalisé plusieurs fois. Ceci revient à dupliquer le souhait en question.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_besoins_fonctionnels">23. Besoins fonctionnels</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_gestion_des_utilisateurs">23.1. Gestion des utilisateurs</h3>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>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.</p>
</div>
</div>
<div class="sect2">
<h3 id="_gestion_des_listes">23.2. Gestion des listes</h3>
<div class="sect3">
<h4 id="_modèlisation">23.2.1. Modèlisation</h4>
<div class="paragraph">
<p>Les données suivantes doivent être associées à une liste:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>un identifiant</p>
</li>
<li>
<p>un identifiant externe (un GUID, par exemple)</p>
</li>
<li>
<p>un nom</p>
</li>
<li>
<p>une description</p>
</li>
<li>
<p>le propriétaire, associé à l&#8217;utilisateur qui l&#8217;aura créée</p>
</li>
<li>
<p>une date de création</p>
</li>
<li>
<p>une date de modification</p>
</li>
</ul>
</div>
</div>
<div class="sect3">
<h4 id="_fonctionnalités">23.2.2. Fonctionnalités</h4>
<div class="ulist">
<ul>
<li>
<p>Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer une liste dont il est le propriétaire</p>
</li>
<li>
<p>Un utilisateur doit pouvoir associer ou retirer des souhaits à une liste dont il est le propriétaire</p>
</li>
<li>
<p>Il faut pouvoir accéder à une liste, avec un utilisateur authentifier ou non, <strong>via</strong> son identifiant externe</p>
</li>
<li>
<p>Il faut pouvoir envoyer un email avec le lien vers la liste, contenant son identifiant externe</p>
</li>
<li>
<p>L&#8217;utilisateur doit pouvoir voir toutes les listes qui lui appartiennent</p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_gestion_des_souhaits">23.3. Gestion des souhaits</h3>
<div class="sect3">
<h4 id="_modélisation_3">23.3.1. Modélisation</h4>
<div class="paragraph">
<p>Les données suivantes peuvent être associées à un souhait:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>un identifiant</p>
</li>
<li>
<p>identifiant de la liste</p>
</li>
<li>
<p>un nom</p>
</li>
<li>
<p>une description</p>
</li>
<li>
<p>le propriétaire</p>
</li>
<li>
<p>une date de création</p>
</li>
<li>
<p>une date de modification</p>
</li>
<li>
<p>une image, afin de représenter l&#8217;objet ou l&#8217;idée</p>
</li>
<li>
<p>un nombre (1 par défaut)</p>
</li>
<li>
<p>un prix facultatif</p>
</li>
<li>
<p>un nombre de part, facultatif également, si un prix est fourni.</p>
</li>
</ul>
</div>
</div>
<div class="sect3">
<h4 id="_fonctionnalités_2">23.3.2. Fonctionnalités</h4>
<div class="ulist">
<ul>
<li>
<p>Un utilisateur authentifié doit pouvoir créer, modifier, désactiver et supprimer un souhait dont il est le propriétaire.</p>
</li>
<li>
<p>On ne peut créer un souhait sans liste associée</p>
</li>
<li>
<p>Il faut pouvoir fractionner un souhait uniquement si un prix est donné.</p>
</li>
<li>
<p>Il faut pouvoir accéder à un souhait, avec un utilisateur authentifié ou non.</p>
</li>
<li>
<p>Il faut pouvoir réaliser un souhait ou une partie seulement, avec un utilisateur authentifié ou non.</p>
</li>
<li>
<p>Un souhait en cours de réalisation et composé de différentes parts ne peut plus être modifié.</p>
</li>
<li>
<p>Un souhait en cours de réalisation ou réalisé ne peut plus être supprimé.</p>
</li>
<li>
<p>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.</p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_gestion_des_réalisations_de_souhaits">23.4. Gestion des réalisations de souhaits</h3>
<div class="sect3">
<h4 id="_modélisation_4">23.4.1. Modélisation</h4>
<div class="paragraph">
<p>Les données suivantes peuvent être associées à une réalisation de souhait:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>identifiant du souhait</p>
</li>
<li>
<p>identifiant de l&#8217;utilisateur si connu</p>
</li>
<li>
<p>identifiant de la personne si utilisateur non connu</p>
</li>
<li>
<p>un commentaire</p>
</li>
<li>
<p>une date de réalisation</p>
</li>
</ul>
</div>
</div>
<div class="sect3">
<h4 id="_fonctionnalités_3">23.4.2. Fonctionnalités</h4>
<div class="ulist">
<ul>
<li>
<p>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%.</p>
</li>
<li>
<p>L&#8217;utilisateur doit pouvoir voir la ou les personnes ayant réalisé un souhait.</p>
</li>
<li>
<p>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é.</p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_gestion_des_personnes_réalisants_les_souhaits_et_qui_ne_sont_pas_connues">23.5. Gestion des personnes réalisants les souhaits et qui ne sont pas connues</h3>
<div class="sect3">
<h4 id="_modélisation_5">23.5.1. Modélisation</h4>
<div class="paragraph">
<p>Les données suivantes peuvent être associées à une personne réalisant un souhait:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>un identifiant</p>
</li>
<li>
<p>un nom</p>
</li>
<li>
<p>une adresse email facultative</p>
</li>
</ul>
</div>
</div>
<div class="sect3">
<h4 id="_fonctionnalités_4">23.5.2. Fonctionnalités</h4>
<div class="sidebarblock">
<div class="content">
<div class="paragraph">
<p>Modélisation</p>
</div>
</div>
</div>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>Comme on l&#8217;a vu dans la description des fonctionnalités, on va <strong>grosso modo</strong> avoir besoin des éléments suivants:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Des listes de souhaits</p>
</li>
<li>
<p>Des éléments qui composent ces listes</p>
</li>
<li>
<p>Des parts pouvant composer chacun de ces éléments</p>
</li>
<li>
<p>Des utilisateurs pour gérer tout ceci.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>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:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre># wish/models.py</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>from django.db import models</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>class Wishlist(models.Model):
pass</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>class Item(models.Model):
pass</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>class Part(models.Model):
pass</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Les classes sont créées, mais vides. Entrons dans les détails.</p>
</div>
<div class="sidebarblock">
<div class="content">
<div class="paragraph">
<p>Listes de souhaits</p>
</div>
</div>
</div>
<div class="paragraph">
<p>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:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>un identifiant</p>
</li>
<li>
<p>un identifiant externe</p>
</li>
<li>
<p>un nom</p>
</li>
<li>
<p>une description</p>
</li>
<li>
<p>une date de création</p>
</li>
<li>
<p>une date de modification</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Notre classe <code>Wishlist</code> peut être définie de la manière suivante:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre># wish/models.py</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>class Wishlist(models.Model):</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>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)</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>Que peut-on constater?</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Que s&#8217;il n&#8217;est pas spécifié, un identifiant <code>id</code> 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 <code>primary_key=True</code>.</p>
</li>
<li>
<p>Que chaque type de champs (<code>DateTimeField</code>, <code>CharField</code>, <code>UUIDField</code>, 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.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Au niveau de notre modélisation:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>La propriété <code>created_at</code> est gérée automatiquement par Django grâce à l&#8217;attribut <code>auto_now_add</code>: de cette manière, lors d&#8217;un <strong>ajout</strong>, une valeur par défaut ("<strong>maintenant</strong>") sera attribuée à cette propriété.</p>
</li>
<li>
<p>La propriété <code>updated_at</code> est également gérée automatique, cette fois grâce à l&#8217;attribut <code>auto_now</code> initialisé à <code>True</code>: lors d&#8217;une <strong>mise à jour</strong>, 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 <strong>quels champs</strong> ont été modifiés, mais cela nous conviendra dans un premier temps.</p>
</li>
<li>
<p>La propriété <code>external_id</code> est de type <code>UUIDField</code>. Lorsqu&#8217;une nouvelle instance sera instanciée, cette propriété prendra la valeur générée par la fonction <code>uuid.uuid4()</code>. <strong>A priori</strong>, chacun des types de champs possède une propriété <code>default</code>, qui permet d&#8217;initialiser une valeur sur une nouvelle instance.</p>
</li>
</ul>
</div>
<div class="sidebarblock">
<div class="content">
<div class="paragraph">
<p>Souhaits</p>
</div>
</div>
</div>
<div class="paragraph">
<p>Nos souhaits ont besoin des propriétés suivantes:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>un identifiant</p>
</li>
<li>
<p>l&#8217;identifiant de la liste auquel le souhait est lié</p>
</li>
<li>
<p>un nom</p>
</li>
<li>
<p>une description</p>
</li>
<li>
<p>le propriétaire</p>
</li>
<li>
<p>une date de création</p>
</li>
<li>
<p>une date de modification</p>
</li>
<li>
<p>une image permettant de le représenter.</p>
</li>
<li>
<p>un nombre (1 par défaut)</p>
</li>
<li>
<p>un prix facultatif</p>
</li>
<li>
<p>un nombre de part facultatif, si un prix est fourni.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Après implémentation, cela ressemble à ceci:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre># wish/models.py</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>class Wish(models.Model):</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>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)</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>A nouveau, que peut-on constater ?</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Les clés étrangères sont gérées directement dans la déclaration du modèle. Un champ de type `ForeignKey &lt;<a href="https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ForeignKey&gt;`_" class="bare">https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ForeignKey&gt;`_</a> 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;<a href="https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/&gt;`" class="bare">https://docs.djangoproject.com/en/1.8/topics/db/examples/one_to_one/&gt;`</a><em>, alors qu&#8217;une relation N-N utilisera un `ManyToManyField &lt;<a href="https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/&gt;`" class="bare">https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_many/&gt;`</a></em>.</p>
</li>
<li>
<p>L&#8217;attribut <code>default</code> permet de spécifier une valeur initiale, utilisée lors de la construction de l&#8217;instance. Cet attribut peut également être une fonction.</p>
</li>
<li>
<p>Pour rendre un champ optionnel, il suffit de lui ajouter l&#8217;attribut <code>null=True</code>.</p>
</li>
<li>
<p>Comme cité ci-dessus, chaque champ possède des attributs spécifiques. Le champ <code>DecimalField</code> possède par exemple les attributs <code>max_digits</code> et <code>decimal_places</code>, qui nous permettra de représenter une valeur comprise entre 0 et plus d&#8217;un milliard (avec deux chiffres décimaux).</p>
</li>
<li>
<p>L&#8217;ajout d&#8217;un champ de type <code>ImageField</code> nécessite l&#8217;installation de <code>pillow</code> pour la gestion des images. Nous l&#8217;ajoutons donc à nos pré-requis, dans le fichier <code>requirements/base.txt</code>.</p>
</li>
</ul>
</div>
<div class="sidebarblock">
<div class="content">
<div class="paragraph">
<p>Parts</p>
</div>
</div>
</div>
<div class="paragraph">
<p>Les parts ont besoins des propriétés suivantes:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>un identifiant</p>
</li>
<li>
<p>identifiant du souhait</p>
</li>
<li>
<p>identifiant de l&#8217;utilisateur si connu</p>
</li>
<li>
<p>identifiant de la personne si utilisateur non connu</p>
</li>
<li>
<p>un commentaire</p>
</li>
<li>
<p>une date de réalisation</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>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:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre>from django.contrib.auth.models import User</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>class WishPart(models.Model):</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>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)</pre>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>La classe <code>User</code> 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 <code>request.user</code>, transmis à chaque fonction ou <strong>Class-based-view</strong>. C&#8217;est un des avantages d&#8217;un framework tout intégré: il vient <strong>batteries-included</strong> 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.</p>
</div>
<div class="paragraph">
<p>La classe <code>UnknownUser</code> permet de représenter un utilisateur non enregistré sur le site et est définie au point suivant.</p>
</div>
<div class="sidebarblock">
<div class="content">
<div class="paragraph">
<p>Utilisateurs inconnus</p>
</div>
</div>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>todo:: je supprimerais pour que tous les utilisateurs soient gérés au même endroit.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>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:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>un identifiant</p>
</li>
<li>
<p>un nom</p>
</li>
<li>
<p>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.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Ceci nous donne après implémentation:</p>
</div>
<div class="olist loweralpha">
<ol class="loweralpha" type="a">
<li>
<p>code-block:: python</p>
<div class="literalblock">
<div class="content">
<pre>class UnkownUser(models.Model):</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>name = models.CharField(max_length=255)
email = models.CharField(email = models.CharField(max_length=255, unique=True)</pre>
</div>
</div>
</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_tests_unitaires_2">24. Tests unitaires</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_pourquoi_sennuyer_à_écrire_des_tests">24.1. Pourquoi s&#8217;ennuyer à écrire des tests?</h3>
<div class="paragraph">
<p>Traduit grossièrement depuis un article sur `https://medium.com &lt;<a href="https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21&gt;`_" class="bare">https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21&gt;`_</a>:</p>
</div>
<div class="literalblock">
<div class="content">
<pre>Vos tests sont la première et la meilleure ligne de défense contre les défauts de programmation. Ils sont</pre>
</div>
</div>
<div class="literalblock">
<div class="content">
<pre>Les tests unitaires combinent de nombreuses fonctionnalités, qui en fait une arme secrète au service d'un développement réussi:</pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Aide au design: écrire des tests avant d&#8217;écrire le code vous donnera une meilleure perspective sur le design à appliquer aux API.</p>
</li>
<li>
<p>Documentation (pour les développeurs): chaque description d&#8217;un test</p>
</li>
<li>
<p>Tester votre compréhension en tant que développeur:</p>
</li>
<li>
<p>Assurance qualité: des tests,
5.</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_why_bother_with_test_discipline">24.2. Why Bother with Test Discipline?</h3>
<div class="paragraph">
<p>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).</p>
</div>
<div class="paragraph">
<p>Unit tests combine many features that make them your secret weapon to application success:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Design aid: Writing tests first gives you a clearer perspective on the ideal API design.</p>
</li>
<li>
<p>Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.</p>
</li>
<li>
<p>Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?</p>
</li>
<li>
<p>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.</p>
</li>
<li>
<p>Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>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.</p>
</div>
</div>
<div class="sect2">
<h3 id="_what_are_you_testing">24.3. What are you testing?</h3>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>What component aspect are you testing?</p>
</li>
<li>
<p>What should the feature do? What specific behavior requirement are you testing?</p>
</li>
</ol>
</div>
</div>
<div class="sect2">
<h3 id="_couverture_de_code_2">24.4. Couverture de code</h3>
<div class="paragraph">
<p>On a vu au chapitre 1 qu&#8217;il était possible d&#8217;obtenir une couverture de code, c&#8217;est-à-dire un pourcentage.</p>
</div>
</div>
<div class="sect2">
<h3 id="_comment_tester">24.5. Comment tester ?</h3>
<div class="paragraph">
<p>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.</p>
</div>
<div class="paragraph">
<p>Pour l&#8217;exemple, nous allons écrire la fonction <code>percentage_of_completion</code> sur la classe <code>Wish</code>, 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:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">class Wish(models.Model):
[...]
@property
def percentage_of_completion(self):
"""
Calcule le pourcentage de complétion pour un élément.
"""
number_of_linked_parts = WishPart.objects.filter(wish=self).count()
total = self.number_of_parts * self.numbers_available
percentage = (number_of_linked_parts / total)
return percentage * 100</code></pre>
</div>
</div>
<div class="paragraph">
<p>Lancez maintenant la couverture de code. Vous obtiendrez ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-text" data-lang="text">$ coverage run --source "." src/manage.py test wish
$ coverage report
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 36 5 0 0 88%
------------------------------------------------------------------
TOTAL 69 5 4 1 93%</code></pre>
</div>
</div>
<div class="paragraph">
<p>Si vous générez le rapport HTML avec la commande <code>coverage html</code> et que vous ouvrez le fichier <code>coverage_html_report/src_wish_models_py.html</code>, vous verrez que les méthodes en rouge ne sont pas testées.
<strong>A contrario</strong>, la couverture de code atteignait <strong>98%</strong> avant l&#8217;ajout de cette nouvelle méthode.</p>
</div>
<div class="paragraph">
<p>Pour cela, on va utiliser un fichier <code>tests.py</code> dans notre application <code>wish</code>. <strong>A priori</strong>, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">from django.test import TestCase
class TestWishModel(TestCase):
def test_percentage_of_completion(self):
"""
Vérifie que le pourcentage de complétion d'un souhait
est correctement calculé.
Sur base d'un souhait, on crée quatre parts et on vérifie
que les valeurs s'étalent correctement sur 25%, 50%, 75% et 100%.
"""
wishlist = Wishlist(name='Fake WishList',
description='This is a faked wishlist')
wishlist.save()
wish = Wish(wishlist=wishlist,
name='Fake Wish',
description='This is a faked wish',
number_of_parts=4)
wish.save()
part1 = WishPart(wish=wish, comment='part1')
part1.save()
self.assertEqual(25, wish.percentage_of_completion)
part2 = WishPart(wish=wish, comment='part2')
part2.save()
self.assertEqual(50, wish.percentage_of_completion)
part3 = WishPart(wish=wish, comment='part3')
part3.save()
self.assertEqual(75, wish.percentage_of_completion)
part4 = WishPart(wish=wish, comment='part4')
part4.save()
self.assertEqual(100, wish.percentage_of_completion)</code></pre>
</div>
</div>
<div class="paragraph">
<p>L&#8217;attribut <code>@property</code> sur la méthode <code>percentage_of_completion()</code> va nous permettre d&#8217;appeler directement la méthode <code>percentage_of_completion()</code> comme s&#8217;il s&#8217;agissait d&#8217;une propriété de la classe, au même titre que les champs <code>number_of_parts</code> ou <code>numbers_available</code>. 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 <strong>cache</strong>, que nous aborderons plus loin.</p>
</div>
<div class="paragraph">
<p>En relançant la couverture de code, on voit à présent que nous arrivons à 99%:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-shell" data-lang="shell">$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 34 0 0 0 100%
src\wish\tests.py 20 0 0 0 100%
------------------------------------------------------------------
TOTAL 87 0 4 1 99%</code></pre>
</div>
</div>
<div class="paragraph">
<p>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.</p>
</div>
</div>
<div class="sect2">
<h3 id="_quelques_liens_utiles">24.6. Quelques liens utiles</h3>
<div class="ulist">
<ul>
<li>
<p>`Django factory boy &lt;<a href="https://github.com/rbarrois/django-factory_boy/tree/v1.0.0&gt;`_" class="bare">https://github.com/rbarrois/django-factory_boy/tree/v1.0.0&gt;`_</a></p>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_a_retenir">25. A retenir</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_constructeurs">25.1. Constructeurs</h3>
<div class="paragraph">
<p>Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode <code><em>init</em></code>: créez plutôt une méthode static de type <code>create()</code>, en y associant les paramètres obligatoires ou souhaités:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python">class Wishlist(models.Model):
@staticmethod
def create(name, description):
w = Wishlist()
w.name = name
w.description = description
w.save()
return w
class Item(models.Model):
@staticmethod
def create(name, description, wishlist):
i = Item()
i.name = name
i.description = description
i.wishlist = wishlist
i.save()
return i</code></pre>
</div>
</div>
<div class="paragraph">
<p>Mieux encore: on pourrait passer par un <code>ModelManager</code> pour limiter le couplage; l''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.</p>
</div>
</div>
<div class="sect2">
<h3 id="_relations">25.2. Relations</h3>
<div class="sect3">
<h4 id="_types_de_relations">25.2.1. Types de relations</h4>
<div class="ulist">
<ul>
<li>
<p>ForeignKey</p>
</li>
<li>
<p>ManyToManyField</p>
</li>
<li>
<p>OneToOneField</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des <strong>ForeignKey</strong> d&#8217;une classe A vers une classe B. Il existe également les champs de type <strong>ManyToManyField</strong>, afin de représenter une relation N-N. Les champs de type <strong>OneToOneField</strong>, pour représenter une relation 1-1.
Dans notre modèle ci-dessus, nous n&#8217;avons jusqu&#8217;à présent eu besoin que des relations 1-N: la première entre les listes de souhaits et les souhaits; la seconde entre les souhaits et les parts.</p>
</div>
</div>
<div class="sect3">
<h4 id="_mise_en_pratique">25.2.2. Mise en pratique</h4>
<div class="paragraph">
<p>Dans le cas de nos listes et de leurs souhaits, on a la relation suivante:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist)</code></pre>
</div>
</div>
<div class="paragraph">
<p>Depuis le code, à partir de l&#8217;instance de la classe <code>Item</code>, on peut donc accéder à la liste en appelant la propriété <code>wishlist</code> de notre instance. <strong>A contrario</strong>, depuis une instance de type <code>Wishlist</code>, on peut accéder à tous les éléments liés grâce à <code>&lt;nom de la propriété&gt;_set</code>; ici <code>item_set</code>.</p>
</div>
<div class="paragraph">
<p>Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l&#8217;attribut <code>related_name</code> afin de nommer la relation inverse.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># wish/models.py
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist, related_name='items')</code></pre>
</div>
</div>
<div class="paragraph">
<p>A partir de maintenant, on peut accéder à nos propriétés de la manière suivante:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># python manage.py shell
&gt;&gt;&gt; from wish.models import Wishlist, Item
&gt;&gt;&gt; w = Wishlist('Liste de test', 'description')
&gt;&gt;&gt; w = Wishlist.create('Liste de test', 'description')
&gt;&gt;&gt; i = Item.create('Element de test', 'description', w)
&gt;&gt;&gt;
&gt;&gt;&gt; i.wishlist
&lt;Wishlist: Wishlist object&gt;
&gt;&gt;&gt;
&gt;&gt;&gt; w.items.all()
[&lt;Item: Item object&gt;]</code></pre>
</div>
</div>
<div class="paragraph">
<p>Remarque: 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 et pour gagner en cohérence, on fixe alors une valeur à l&#8217;attribut <code>related_name</code>.</p>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_querysets_managers">25.3. Querysets &amp; managers</h3>
<div class="ulist">
<ul>
<li>
<p><a href="http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm" class="bare">http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm</a></p>
</li>
<li>
<p><a href="https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator" class="bare">https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator</a></p>
</li>
<li>
<p><a href="http://blog.etianen.com/blog/2013/06/08/django-querysets/" class="bare">http://blog.etianen.com/blog/2013/06/08/django-querysets/</a></p>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_refactoring">26. Refactoring</h2>
<div class="sectionbody">
<div class="paragraph">
<p>On constate que plusieurs classes possèdent les mêmes propriétés <code>created_at</code> et <code>updated_at</code>, 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 <code>Wishlist</code>, <code>Item</code> et <code>Part</code> en héritent. Django gère trois sortes d&#8217;héritage:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>L&#8217;héritage par classe abstraite</p>
</li>
<li>
<p>L&#8217;héritage classique</p>
</li>
<li>
<p>L&#8217;héritage par classe proxy.</p>
</li>
</ul>
</div>
<div class="sect2">
<h3 id="_classe_abstraite">26.1. Classe abstraite</h3>
<div class="paragraph">
<p>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.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># wish/models.py
class AbstractModel(models.Model):
class Meta:
abstract = True
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Wishlist(AbstractModel):
pass
class Item(AbstractModel):
pass
class Part(AbstractModel):
pass</code></pre>
</div>
</div>
<div class="paragraph">
<p>En traduisant ceci en SQL, on aura en fait trois tables, chacune reprenant les champs <code>created_at</code> et <code>updated_at</code>, ainsi que son propre identifiant:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-sql" data-lang="sql">--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_wishlist" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_item" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_part" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
COMMIT;</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_héritage_classique">26.2. Héritage classique</h3>
<div class="paragraph">
<p>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 <code>abstract = True</code>, on se retrouvera en fait avec quatre tables SQL:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>Une table <code>AbstractModel</code>, qui reprend les deux champs <code>created_at</code> et <code>updated_at</code></p>
</li>
<li>
<p>Une table <code>Wishlist</code></p>
</li>
<li>
<p>Une table <code>Item</code></p>
</li>
<li>
<p>Une table <code>Part</code>.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>A nouveau, en analysant la sortie SQL de cette modélisation, on obtient ceci:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-sql" data-lang="sql">--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_abstractmodel" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_wishlist" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_item" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_part" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "wish_abstractmodel" ("id")
)
;
COMMIT;</code></pre>
</div>
</div>
<div class="paragraph">
<p>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 <strong>join</strong> sur la classe mère.</p>
</div>
<div class="paragraph">
<p>Dans ce sens, cela va encore&#8230;&#8203; Mais imaginez que vous définissiez une classe <code>Wishlist</code>, de laquelle héritent les classes <code>ChristmasWishlist</code> et <code>EasterWishlist</code>: pour obtenir la liste complètes des listes de souhaits, il vous faudra faire une jointure <strong>externe</strong> 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.</p>
</div>
</div>
<div class="sect2">
<h3 id="_classe_proxy">26.3. Classe proxy</h3>
<div class="paragraph">
<p>Lorsqu&#8217;on définit une classe de type <strong>proxy</strong>, 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.</p>
</div>
<div class="paragraph">
<p>Nous pourrions ainsi définir les classes suivantes:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"># wish/models.py
class Wishlist(models.Model):
name = models.CharField(max_length=255)
description = models.CharField(max_length=2000)
expiration_date = models.DateField()
@staticmethod
def create(self, name, description, expiration_date=None):
wishlist = Wishlist()
wishlist.name = name
wishlist.description = description
wishlist.expiration_date = expiration_date
wishlist.save()
return wishlist
class ChristmasWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
christmas = datetime(current_year, 12, 31)
w = Wishlist.create(name, description, christmas)
w.save()
class EasterWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
expiration_date = datetime(current_year, 4, 1)
w = Wishlist.create(name, description, expiration_date)
w.save()</code></pre>
</div>
</div>
<div class="sidebarblock">
<div class="content">
<div class="paragraph">
<p>Gestion des utilisateurs</p>
</div>
</div>
</div>
<div class="paragraph">
<p>Dans les spécifications, nous souhaitions pouvoir associer un utilisateur à une liste (<strong>le propriétaire</strong>) et un utilisateur à une part (<strong>le donateur</strong>). 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: <code>AUTH_USER_MODEL</code>.</p>
</div>
<div class="sidebarblock">
<div class="content">
<div class="paragraph">
<p>Jouons un peu avec la console</p>
</div>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_métamodèle">Métamodèle</h3>
<div class="paragraph">
<p>Sous ce titre franchement pompeux, on va un peu parler de la modélisation du modèle. Quand on prend une classe (par exemple, <code>Wishlist</code> que l&#8217;on a défini ci-dessus), on voit qu&#8217;elle hérite par défaut de <code>models.Model</code>. On peut regarder les propriétés définies dans cette classe en analysant le fichier <code>lib\site-packages\django\models\base.py</code>. On y voit notamment que <code>models.Model</code> hérite de <code>ModelBase</code> au travers de `six &lt;<a href="https://pypi.python.org/pypi/six&gt;`_" class="bare">https://pypi.python.org/pypi/six&gt;`_</a> pour la rétrocompatibilité vers Python 2.7.</p>
</div>
<div class="paragraph">
<p>Cet héritage apporte notamment les fonctions <code>save()</code>, <code>clean()</code>, <code>delete()</code>, &#8230;&#8203; Bref, toutes les méthodes qui font qu&#8217;une instance est sait <strong>comment</strong> interagir avec la base de données. La base d&#8217;un `ORM &lt;<a href="https://en.wikipedia.org/wiki/Object-relational_mapping&gt;`_" class="bare">https://en.wikipedia.org/wiki/Object-relational_mapping&gt;`_</a>, en fait.</p>
</div>
<div class="paragraph">
<p>D&#8217;autre part, chaque classe héritant de <code>models.Model</code> possède une propriété <code>objects</code>. Comme on l&#8217;a vu dans la section <strong>Jouons un peu avec la console</strong>, cette propriété permet d&#8217;accéder aux objects persistants dans la base de données.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_supervision_des_logs">27. Supervision des logs</h2>
<div class="sectionbody">
</div>
</div>
<div class="sect1">
<h2 id="_feedbacks_utilisateurs">28. feedbacks utilisateurs</h2>
<div class="sectionbody">
</div>
</div>
<h1 id="_en_bonus" class="sect0">En bonus</h1>
<div class="sect1">
<h2 id="_snippets_utiles_et_forcément_dispensables">29. Snippets utiles (et forcément dispensables)</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_récupération_du_dernier_tag_git_en_python">29.1. Récupération du dernier tag Git en Python</h3>
<div class="paragraph">
<p>L&#8217;idée ici est simplement de pouvoir afficher le numéro de version ou le hash d&#8217;exécution du code, sans avoir à se connecter au dépôt. Cela apporte une certaine transparence, <strong>sous réserve que le code soit géré par Git</strong>. Si vous suivez scrupuleusement les 12 facteurs, la version de l&#8217;application déployée n&#8217;est plus sensée conserver un lien avec votre dépôt d&#8217;origine&#8230;&#8203; Si vous déployez votre code en utilisant un <code>git fetch</code> puis un <code>git checkout &lt;tag_name&gt;</code>, le morceau de code ci-dessous pourra vous intéresser :-)</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-python" data-lang="python"></code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated 2020-02-17 20:07:52 +0100
</div>
</div>
</body>
</html>