rst - adoc

This commit is contained in:
Fred 2020-02-17 20:45:39 +01:00
parent 8eb38628d0
commit 5a027cba52
15 changed files with 2011 additions and 1761 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

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

View File

@ -1,158 +1,165 @@
************
Mise en page
************
=== Mise en page
Pour que nos pages soient un peu plus *eye-candy* que ce qu'on a présenté ci-dessus, nous allons modifié notre squelette pour qu'il se base sur `Bootstrap <http://getbootstrap.com/>`_. Nous placerons une barre de navigation principale, la possibilité de se connecter pour l'utilisateur et définirons quelques emplacements à utiliser par la suite. Reprenez votre fichier `base.html` et modifiez le comme ceci:
Pour que nos pages soient un peu plus *eye-candy* que ce qu'on a présenté ci-dessus, nous allons modifié notre squelette pour qu'il se base sur `Bootstrap <http://getbootstrap.com/>`_. Nous placerons une barre de navigation principale, la possibilité de se connecter pour l'utilisateur et définirons quelques emplacements à utiliser par la suite. Reprenez votre fichier ``base.html`` et modifiez le comme ceci:
[source,html]
----
.. code-block:: html
{% load staticfiles %}
{% load staticfiles %}
<!DOCTYPE html>
<!--[if IE 9]><html class="lt-ie10" lang="en" > <![endif]-->
<html class="no-js" lang="en">
<!DOCTYPE html>
<!--[if IE 9]><html class="lt-ie10" lang="en" > <![endif]-->
<html class="no-js" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
<script src="//code.jquery.com/jquery.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href="{% static 'css/style.css' %}" rel="stylesheet">
<link rel="icon" href="{% static 'img/favicon.ico' %}" />
<title>Gwift</title>
</head>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
<script src="//code.jquery.com/jquery.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href="{% static 'css/style.css' %}" rel="stylesheet">
<link rel="icon" href="{% static 'img/favicon.ico' %}" />
<title>Gwift</title>
</head>
<body class="base-body">
<body class="base-body">
<!-- navigation -->
<div class="nav-wrapper">
<div id="nav">
<nav class="navbar navbar-default navbar-static-top navbar-shadow">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#menuNavbar">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="{% static 'img/gwift-20x20.png' %}" />
</a>
</div>
<div class="collapse navbar-collapse" id="menuNavbar">
{% include "_menu_items.html" %}
</div>
<!-- navigation -->
<div class="nav-wrapper">
<div id="nav">
<nav class="navbar navbar-default navbar-static-top navbar-shadow">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#menuNavbar">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="{% static 'img/gwift-20x20.png' %}" />
</a>
</div>
<div class="collapse navbar-collapse" id="menuNavbar">
{% include "_menu_items.html" %}
</div>
</nav>
</div>
</div>
<!-- end navigation -->
<!-- content -->
<div class="container">
<div class="row">
<div class="col-md-8">
{% block content %}{% endblock %}
</div>
</nav>
</div>
</div>
<!-- end navigation -->
<!-- content -->
<div class="container">
<div class="row">
<div class="col-md-8">
{% block content %}{% endblock %}
</div>
</div>
<!-- end content -->
<!-- footer -->
<footer class="footer">
{% include "_footer.html" %}
</footer>
<!-- end footer -->
</body>
</html>
</div>
<!-- end content -->
<!-- footer -->
<footer class="footer">
{% include "_footer.html" %}
</footer>
<!-- end footer -->
</body>
</html>
----
Quelques remarques:
* La première ligne du fichier inclut le *tag* ``{% load staticfiles %}``. 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 ``{% static 'img/header.png' %}`` ou ``{% static 'css/app_style.css' %}``.
* La balise ``<head />`` est bourée d'appel vers des ressources stockées sur des :abbr:`CDN (Content Delivery Networks)`.
* Les balises ``{% block content %} {% endblock %}`` permettent de faire hériter du contenu depuis une autre page. On l'utilise notamment dans notre page ``templates/wish/list.html``.
* Pour l'entête et le bas de page, on fait appel aux balises ``{% include 'nom_du_fichier.html' %}``: ces fichiers sont des fichiers physiques, placés sur le filesystem, juste à côté du fichier ``base.html``. De façon bête et méchante, cela inclut juste du contenu HTML. Le contenu des fichiers ``_menu_items.html`` et ``_footer.html`` est copié ci-dessous.
* La première ligne du fichier inclut le *tag* `{% load staticfiles %}`. 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 `{% static 'img/header.png' %}` ou `{% static 'css/app_style.css' %}`.
* La balise `<head />` est bourée d'appel vers des ressources stockées sur des :abbr:`CDN (Content Delivery Networks)`.
* Les balises `{% block content %} {% endblock %}` permettent de faire hériter du contenu depuis une autre page. On l'utilise notamment dans notre page `templates/wish/list.html`.
* Pour l'entête et le bas de page, on fait appel aux balises `{% include 'nom_du_fichier.html' %}`: ces fichiers sont des fichiers physiques, placés sur le filesystem, juste à côté du fichier `base.html`. De façon bête et méchante, cela inclut juste du contenu HTML. Le contenu des fichiers `_menu_items.html` et `_footer.html` est copié ci-dessous.
.. code-block:: html
[source,html]
----
<!-- gwift/templates/wish/list.html -->
<!-- gwift/templates/wish/list.html -->
{% extends "base.html" %}
{% extends "base.html" %}
{% block content %}
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
{% endblock %}
.. code-block:: html
<!-- gwift/templates/_menu_items.html -->
<ul class="nav navbar-nav">
<li class="">
<a href="#">
<i class="fa fa-calendar"></i> Mes listes
</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="">
<a href="#">
<i class="fa fa-user"></i> Login / Register
</a>
</li>
{% block content %}
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
{% endblock %}
----
.. code-block:: html
<!-- gwift/templates/_footer.html -->
<div class="container">
Copylefted '16
</div>
[source,html]
----
<!-- gwift/templates/_menu_items.html -->
<ul class="nav navbar-nav">
<li class="">
<a href="#">
<i class="fa fa-calendar"></i> Mes listes
</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="">
<a href="#">
<i class="fa fa-user"></i> Login / Register
</a>
</li>
</ul>
----
[source,html]
----
<!-- gwift/templates/_footer.html -->
<div class="container">
Copylefted '16
</div>
----
En fonction de vos affinités, vous pourriez également passer par `PluCSS <http://plucss.pluxml.org/>`_, `Pure <http://purecss.io/>`_, `Knacss <http://knacss.com/>`_, `Cascade <http://www.cascade-framework.com/>`_, `Semantic <http://semantic-ui.com/>`_ ou `Skeleton <http://getskeleton.com/>`_. Pour notre plus grand bonheur, les frameworks de ce type pullulent. Reste à choisir le bon.
*A priori*, si vous relancez le serveur de développement maintenant, vous devriez déjà voir les modifications... Mais pas les images, ni tout autre fichier statique.
Fichiers statiques
==================
==== Fichiers statiques
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'agit des fichiers suivants:
* ``/static/css/style.css``
* ``/static/img/favicon.ico``
* ``/static/img/gwift-20x20.png``.
* `/static/css/style.css`
* `/static/img/favicon.ico`
* `/static/img/gwift-20x20.png`.
En fait, par défaut, les fichiers statiques sont récupérés grâce à deux handlers: ``django.contrib.staticfiles.finders.FileSystemFinder`` et ``django.contrib.staticfiles.finders.AppDirectoriesFinder``. En fait, Django va considérer un répertoire ``static`` à l'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 ``gwift/static``. On y trouve donc:
En fait, par défaut, les fichiers statiques sont récupérés grâce à deux handlers:
.. code-block:: shell
. `django.contrib.staticfiles.finders.FileSystemFinder` et . `django.contrib.staticfiles.finders.AppDirectoriesFinder`.
[inclure un tree du répertoire gwift/static]
En fait, Django va considérer un répertoire `static` à l'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 `gwift/static`. On y trouve donc:
Pour indiquer à Django que vous souhaitez aller y chercher vos fichiers, il faut initialiser la `variable <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_DIRS>`_ ``STATICFILES_DIRS`` dans le fichier ``settings/base.py``. Vérifiez également que la variable ``STATIC_URL`` est correctement définie.
[source,bash]
----
[inclure un tree du répertoire gwift/static]
----
.. code-block:: python
Pour indiquer à Django que vous souhaitez aller y chercher vos fichiers, il faut initialiser la `variable <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_DIRS>`_ `STATICFILES_DIRS` dans le fichier `settings/base.py`. Vérifiez également que la variable `STATIC_URL` est correctement définie.
# gwift/settings/base.py
STATIC_URL = '/static/'
.. code-block:: python
[source,python]
----
# gwift/settings/base.py
# gwift/settings/dev.py
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
STATIC_URL = '/static/'
----
[source,python]
----
# gwift/settings/dev.py
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
----
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'est initialisée que dans le fichier des paramètres liés au développement.

View File

@ -1,130 +1,127 @@
*********
Templates
*********
=== Templates
Avant de commencer à interagir avec nos données au travers de listes, formulaires et IHM sophistiquées, quelques mots sur les templates: il s'agit en fait de *squelettes* de présentation, recevant en entrée un dictionnaire contenant des clés-valeurs et ayant pour but de les afficher dans le format que vous définirez. En intégrant un ensemble de *tags*, cela vous permettra de greffer les données reçues en entrée dans un patron prédéfini.
Une page HTML basique ressemble à ceci:
.. code-block:: html
[source,html]
----
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Hello world!</p>
</body>
</html>
----
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Hello world!</p>
</body>
</html>
Notre première vue permettra de récupérer la liste des objets de type ``Wishlist`` que nous avons définis dans le fichier ``wish/models.py``. Supposez que cette liste soit accessible *via* la clé ``wishlists`` d'un dictionnaire passé au template. Elle devient dès lors accessible grâce aux tags ``{% for wishlist in wishlists %}``. A chaque tour de boucle, on pourra directement accéder à la variable ``{{ wishlist }}``. De même, il sera possible d'accéder aux propriétés de cette objet de la même manière: ``{{ wishlist.id }}``, ``{{ wishlist.description }}``, ... et d'ainsi respecter la mise en page que nous souhaitons.
Notre première vue permettra de récupérer la liste des objets de type `Wishlist` que nous avons définis dans le fichier `wish/models.py`. Supposez que cette liste soit accessible *via* la clé `wishlists` d'un dictionnaire passé au template. Elle devient dès lors accessible grâce aux tags `{% for wishlist in wishlists %}`. A chaque tour de boucle, on pourra directement accéder à la variable `{{ wishlist }}`. De même, il sera possible d'accéder aux propriétés de cette objet de la même manière: `{{ wishlist.id }}`, `{{ wishlist.description }}`, ... et d'ainsi respecter la mise en page que nous souhaitons.
En reprenant l'exemple de la page HTML définie ci-dessus, on pourra l'agrémenter de la manière suivante:
.. code-block:: html
[source,django]
----
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
</body>
</html>
----
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
</body>
</html>
Vous pouvez déjà copier ce contenu dans un fichier `templates/wsh/list.html`, on en aura besoin par la suite.
==== Structure et configuration
Vous pouvez déjà copié ce contenu dans un fichier ``templates/wsh/list.html``, on en aura besoin par la suite.
Il est conseillé que les templates respectent la structure de vos différentes applications, mais dans un répertoire à part. Par convention, nous les placerons dans un répertoire `templates`. La hiérarchie des fichiers devient alors celle-ci:
Structure et configuration
==========================
[source,bash]
----
$ tree templates/
templates/
└── wish
└── list.html
----
Il est conseillé que les templates respectent la structure de vos différentes applications, mais dans un répertoire à part. Par convention, nous les placerons dans un répertoire ``templates``. La hiérarchie des fichiers devient alors celle-ci:
Par défaut, Django cherchera les templates dans les répertoirer d'installation. Vous devrez vous éditer le fichier `gwift/settings.py` et ajouter, dans la variable `TEMPLATES`, la clé `DIRS` de la manière suivante:
.. code--block:: bash
[source,python]
----
TEMPLATES = [
{
...
'DIRS': [ 'templates' ],
...
},
]
----
$ tree templates/
templates/
└── wish
└── list.html
==== Builtins
Par défaut, Django cherchera les templates dans les répertoirer d'installation. Vous devrez vous éditer le fichier ``gwift/settings.py`` et ajouter, dans la variable ``TEMPLATES``, la clé ``DIRS`` de la manière suivante:
Django vient avec un ensemble de *tags*. On a vu la boucle `for` ci-dessus, mais il existe https://docs.djangoproject.com/fr/1.9/ref/templates/builtins/[beaucoup d'autres tags nativement présents]. Les principaux sont par exemple:
.. code-block:: python
* `{% if ... %} ... {% elif ... %} ... {% else %} ... {% endif %}`: permet de vérifier une condition et de n'afficher le contenu du bloc que si la condition est vérifiée.
* Opérateurs de comparaisons: `<`, `>`, `==`, `in`, `not in`.
* Regroupements avec le tag `{% regroup ... by ... as ... %}`.
* `{% url %}` pour construire facilement une URL
* ...
TEMPLATES = [
{
...
'DIRS': [ 'templates' ],
...
},
]
Builtins
========
Django vient avec un ensemble de *tags*. On a vu la boucle ``for`` ci-dessus, mais il existe `beaucoup d'autres tags nativement présents <https://docs.djangoproject.com/fr/1.9/ref/templates/builtins/>`_. Les principaux sont par exemple:
* ``{% if ... %} ... {% elif ... %} ... {% else %} ... {% endif %}``: permet de vérifier une condition et de n'afficher le contenu du bloc que si la condition est vérifiée.
* Opérateurs de comparaisons: ``<``, ``>``, ``==``, ``in``, ``not in``.
* Regroupements avec le tag ``{% regroup ... by ... as ... %}``.
* ``{% url %}`` pour construire facilement une URL
* ...
Non-builtins
============
==== Non-builtins
En plus des quelques tags survolés ci-dessus, il est également possible de construire ses propres tags. La structure est un peu bizarre, car elle consiste à ajouter un paquet dans une de vos applications, à y définir un nouveau module et à y définir un ensemble de fonctions. Chacune de ces fonctions correspondra à un tag appelable depuis vos templates.
Il existe trois types de tags *non-builtins*:
1. Les filtres - on peut les appeler grâce au *pipe* ``|`` directement après une valeur dans le template.
2. Les tags simples - ils peuvent prendre une valeur ou plusieurs en paramètre et retourne une nouvelle valeur. Pour les appeler, c'est *via* les tags ``{% nom_de_la_fonction param1 param2 ... %}``.
3. Les tags d'inclusion: ils retournent un contexte (ie. un dictionnaire), qui est ensuite passé à un nouveau template.
1. Les filtres - on peut les appeler grâce au *pipe* `|` directement après une valeur dans le template.
2. Les tags simples - ils peuvent prendre une valeur ou plusieurs en paramètre et retourne une nouvelle valeur. Pour les appeler, c'est *via* les tags `{% nom_de_la_fonction param1 param2 ... %}`.
3. Les tags d'inclusion: ils retournent un contexte (ie. un dictionnaire), qui est ensuite passé à un nouveau template.
Pour l'implémentation:
1. On prend l'application ``wish`` et on y ajoute un répertoire ``templatetags``, ainsi qu'un fichier ``__init__.py``.
2. Dans ce nouveau paquet, on ajoute un nouveau module que l'on va appeler ``tools.py``
1. On prend l'application `wish` et on y ajoute un répertoire `templatetags`, ainsi qu'un fichier `__init__.py`.
2. Dans ce nouveau paquet, on ajoute un nouveau module que l'on va appeler `tools.py`
3. Dans ce module, pour avoir un aperçu des possibilités, on va définir trois fonctions (une pour chaque type de tags possible).
.. code-block:: shell
[source,bash]
----
[Inclure un tree du dossier template tags]
----
[Inclure un tree du dossier template tags]
.. code-block:: python
[source,python]
----
# wish/tools.py
# 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() }
----
# coding=utf-8
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() }
Pour plus d'informations, la `documentation officielle est un bon début <https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#writing-custom-template-tags>`_.
Pour plus d'informations, la https://docs.djangoproject.com/en/stable/howto/custom-template-tags/#writing-custom-template-tags[documentation officielle est un bon début].

View File

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

View File

@ -1,146 +1,141 @@
****
Vues
****
=== Vues
Une vue correspond à un contrôleur dans le pattern MVC. Tout ce que vous pourrez définir au niveau du fichier ``views.py`` fera le lien entre le modèle stocké dans la base de données et ce avec quoi l'utilisateur pourra réellement interagir (le ``template``).
Une vue correspond à un contrôleur dans le pattern MVC. Tout ce que vous pourrez définir au niveau du fichier `views.py` fera le lien entre le modèle stocké dans la base de données et ce avec quoi l'utilisateur pourra réellement interagir (le `template`).
Chaque vue peut etre représentée de deux manières: soit par des fonctions, soit par des classes. Le comportement leur est propre, mais le résultat reste identique. Le lien entre l'URL à laquelle l'utilisateur accède et son exécution est faite au travers du fichier ``gwift/urls.py``, comme on le verra par la suite.
Chaque vue peut etre représentée de deux manières: soit par des fonctions, soit par des classes. Le comportement leur est propre, mais le résultat reste identique. Le lien entre l'URL à laquelle l'utilisateur accède et son exécution est faite au travers du fichier `gwift/urls.py`, comme on le verra par la suite.
Function Based Views
====================
==== Function Based Views
Les fonctions (ou ``FBV`` pour *Function Based Views*) permettent une implémentation classique des contrôleurs. Au fur et à mesure de votre implémentation, on se rendra compte qu'il y a beaucoup de répétitions dans ce type d'implémentation: elles ne sont pas obsolètes, mais dans certains cas, il sera préférable de passer par les classes.
Les fonctions (ou `FBV` pour *Function Based Views*) permettent une implémentation classique des contrôleurs. Au fur et à mesure de votre implémentation, on se rendra compte qu'il y a beaucoup de répétitions dans ce type d'implémentation: elles ne sont pas obsolètes, mais dans certains cas, il sera préférable de passer par les classes.
Pour définir la liste des ``WishLists`` actuellement disponibles, on précédera de la manière suivante:
Pour définir la liste des `WishLists` actuellement disponibles, on précédera de la manière suivante:
1. Définition d'une fonction qui va récupérer les objets de type ``WishList`` dans notre base de données. La valeur de retour sera la construction d'un dictionnaire (le *contexte*) qui sera passé à un template HTML. On démandera à ce template d'effectuer le rendu au travers de la fonction ``render``, qui est importée par défaut dans le fichier ``views.py``.
2. Construction d'une URL qui permettra de lier l'adresse à l'exécution de la fonction.
3. Définition du squelette.
. Définition d'une fonction qui va récupérer les objets de type `WishList` dans notre base de données. La valeur de retour sera la construction d'un dictionnaire (le *contexte*) qui sera passé à un template HTML. On démandera à ce template d'effectuer le rendu au travers de la fonction `render`, qui est importée par défaut dans le fichier `views.py`.
. Construction d'une URL qui permettra de lier l'adresse à l'exécution de la fonction.
. Définition du squelette.
Définition de la fonction
-------------------------
===== Définition de la fonction
.. code-block:: python
[source,python]
----
# wish/views.py
# wish/views.py
from django.shortcuts import render
from .models import Wishlist
from django.shortcuts import render
from .models import Wishlist
def wishlists(request):
w = Wishlist.objects.all()
return render(request, 'wish/list.html',{ 'wishlists': w })
----
def wishlists(request):
w = Wishlist.objects.all()
return render(request, 'wish/list.html',{ 'wishlists': w })
===== Construction de l'URL
Construction de l'URL
---------------------
[source,python]
----
# gwift/urls.py
.. code-block:: python
from django.conf.urls import include, url
from django.contrib import admin
# gwift/urls.py
from wish import views as wish_views
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', wish_views.wishlists, name='wishlists'),
]
----
from wish import views as wish_views
===== Définition du squelette
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', wish_views.wishlists, name='wishlists'),
]
A ce stade, vérifiez que la variable `TEMPLATES` est correctement initialisée dans le fichier `gwift/settings.py` et que le fichier `templates/wish/list.html` ressemble à ceci:
Définition du squelette
-----------------------
[source,jinj2]
----
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
</body>
</html>
----
A ce stade, vérifiez que la variable ``TEMPLATES`` est correctement initialisée dans le fichier ``gwift/settings.py`` et que le fichier ``templates/wish/list.html`` ressemble à ceci:
.. code-block:: html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title></title>
</head>
<body>
<p>Mes listes de souhaits</p>
<ul>
{% for wishlist in wishlists %}
<li>{{ wishlist.name }}: {{ wishlist.description }}</li>
{% endfor %}
</ul>
</body>
</html>
Exécution
---------
===== Exécution
A présent, ajoutez quelques listes de souhaits grâce à un *shell*, puis lancez le serveur:
.. code-block:: shell
[source,bash]
----
$ python manage.py shell
>>> from wish.models import Wishlist
>>> Wishlist.create('Décembre', "Ma liste pour les fêtes de fin d'année")
<Wishlist: Wishlist object>
>>> Wishlist.create('Anniv 30 ans', "Je suis vieux! Faites des dons!")
<Wishlist: Wishlist object>
----
$ python manage.py shell
>>> from wish.models import Wishlist
>>> Wishlist.create('Décembre', "Ma liste pour les fêtes de fin d'année")
<Wishlist: Wishlist object>
>>> Wishlist.create('Anniv 30 ans', "Je suis vieux! Faites des dons!")
<Wishlist: Wishlist object>
Lancez le serveur grâce à la commande ``python manage.py runserver``, ouvrez un navigateur quelconque et rendez-vous à l'adresse `http://localhost:8000 <http://localhost:8000>`_. Vous devriez obtenir le résultat suivant:
Lancez le serveur grâce à la commande `python manage.py runserver`, ouvrez un navigateur quelconque et rendez-vous à l'adresse `http://localhost:8000 <http://localhost:8000>`_. Vous devriez obtenir le résultat suivant:
.. image:: mvc/my-first-wishlists.png
:align: center
Rien de très sexy, aucune interaction avec l'utilisateur, très peu d'utilisation des variables contextuelles, mais c'est un bon début! =)
Class Based Views
=================
==== Class Based Views
Les classes, de leur côté, implémente le *pattern* objet et permettent d'arriver facilement à un résultat en très peu de temps, parfois même en définissant simplement quelques attributs, et rien d'autre. Pour l'exemple, on va définir deux classes qui donnent exactement le même résultat que la fonction ``wishlists`` ci-dessus. Une première fois en utilisant une classe générique vierge, et ensuite en utilisant une classe de type ``ListView``.
Les classes, de leur côté, implémente le *pattern* objet et permettent d'arriver facilement à un résultat en très peu de temps, parfois même en définissant simplement quelques attributs, et rien d'autre. Pour l'exemple, on va définir deux classes qui donnent exactement le même résultat que la fonction `wishlists` ci-dessus. Une première fois en utilisant une classe générique vierge, et ensuite en utilisant une classe de type `ListView`.
Classe générique
----------------
===== Classe générique
blah
ListView
--------
===== ListView
Les classes génériques implémentent un aspect bien particulier de la représentation d'un modèle, en utilisant très peu d'attributs. Les principales classes génériques sont de type ``ListView``, [...]. L'implémentation consiste, exactement comme pour les fonctions, à:
Les classes génériques implémentent un aspect bien particulier de la représentation d'un modèle, en utilisant très peu d'attributs. Les principales classes génériques sont de type `ListView`, [...]. L'implémentation consiste, exactement comme pour les fonctions, à:
1. Définir la classe
2. Créer l'URL
3. Définir le squelette.
. Définir la classe
. Créer l'URL
. Définir le squelette.
.. code-block:: python
[source,python]
----
# wish/views.py
# wish/views.py
from django.views.generic import ListView
from django.views.generic import ListView
from .models import Wishlist
from .models import Wishlist
class WishListList(ListView):
context_object_name = 'wishlists'
model = Wishlist
template_name = 'wish/list.html'
----
class WishListList(ListView):
context_object_name = 'wishlists'
model = Wishlist
template_name = 'wish/list.html'
[source,python]
----
# gwift/urls.py
from django.conf.urls import include, url
from django.contrib import admin
.. code-block:: python
from wish.views import WishListList
# gwift/urls.py
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]
----
from django.conf.urls import include, url
from django.contrib import admin
C'est tout. Lancez le serveur, le résultat sera identique. Par inférence, Django construit beaucoup d'informations: si on n'avait pas spécifié les variables `context_object_name` et `template_name`, celles-ci auraient pris les valeurs suivantes:
from wish.views import WishListList
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^$', WishListList.as_view(), name='wishlists'),
]
C'est tout. Lancez le serveur, le résultat sera identique. Par inférence, Django construit beaucoup d'informations: si on n'avait pas spécifié les variables ``context_object_name`` et ``template_name``, celles-ci auraient pris les valeurs suivantes:
* ``context_object_name``: ``wishlist_list`` (ou plus précisément, le nom du modèle suivi de ``_list``)
* ``template_name``: ``wish/wishlist_list.html`` (à nouveau, le fichier généré est préfixé du nom du modèle).
* `context_object_name`: `wishlist_list` (ou plus précisément, le nom du modèle suivi de `_list`)
* `template_name`: `wish/wishlist_list.html` (à nouveau, le fichier généré est préfixé du nom du modèle).

View File

@ -1,104 +1,100 @@
*********
A retenir
*********
== A retenir
Constructeurs
=============
=== Constructeurs
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode ``__init__``: créez plutôt une méthode static de type ``create()``, en y associant les paramètres obligatoires ou souhaités:
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode `__init__`: créez plutôt une méthode static de type `create()`, en y associant les paramètres obligatoires ou souhaités:
.. code-block:: python
[source,python]
----
class Wishlist(models.Model):
class Wishlist(models.Model):
@staticmethod
def create(name, description):
w = Wishlist()
w.name = name
w.description = description
w.save()
return w
@staticmethod
def create(name, description):
w = Wishlist()
w.name = name
w.description = description
w.save()
return w
class Item(models.Model):
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
----
@staticmethod
def create(name, description, wishlist):
i = Item()
i.name = name
i.description = description
i.wishlist = wishlist
i.save()
return i
Mieux encore: on pourrait passer par un `ModelManager` pour limiter le couplage; l''accès à une information stockée en base de données ne se ferait dès lors qu'au travers de cette instance et pas directement au travers du modèle. De cette manière, on limite le couplage des classes et on centralise l'accès.
Mieux encore: on pourrait passer par un ``ModelManager`` pour limiter le couplage; l''accès à une information stockée en base de données ne se ferait dès lors qu'au travers de cette instance et pas directement au travers du modèle. De cette manière, on limite le couplage des classes et on centralise l'accès.
=== Relations
Relations
=========
==== Types de relations
Types de relations
------------------
* ForeignKey
* ManyToManyField
* OneToOneField
* ForeignKey
* ManyToManyField
* OneToOneField
Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des **ForeignKey** d'une classe A vers une classe B. Il existe également les champs de type **ManyToManyField**, afin de représenter une relation N-N. Les champs de type **OneToOneField**, pour représenter une relation 1-1.
Dans notre modèle ci-dessus, nous n'avons jusqu'à 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.
Mise en pratique
----------------
==== Mise en pratique
Dans le cas de nos listes et de leurs souhaits, on a la relation suivante:
.. code-block:: python
[source,python]
----
# wish/models.py
# wish/models.py
class Wishlist(models.Model):
pass
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist)
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist)
----
Depuis le code, à partir de l'instance de la classe ``Item``, on peut donc accéder à la liste en appelant la propriété ``wishlist`` de notre instance. *A contrario*, depuis une instance de type ``Wishlist``, on peut accéder à tous les éléments liés grâce à ``<nom de la propriété>_set``; ici ``item_set``.
Depuis le code, à partir de l'instance de la classe `Item`, on peut donc accéder à la liste en appelant la propriété `wishlist` de notre instance. *A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `<nom de la propriété>_set`; ici `item_set`.
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut ``related_name`` afin de nommer la relation inverse.
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut `related_name` afin de nommer la relation inverse.
.. code-block:: python
[source,python]
----
# wish/models.py
# wish/models.py
class Wishlist(models.Model):
pass
class Wishlist(models.Model):
pass
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist, related_name='items')
class Item(models.Model):
wishlist = models.ForeignKey(Wishlist, related_name='items')
----
A partir de maintenant, on peut accéder à nos propriétés de la manière suivante:
.. code-block:: python
[source,python]
----
# python manage.py shell
# python manage.py shell
>>> from wish.models import Wishlist, Item
>>> w = Wishlist('Liste de test', 'description')
>>> w = Wishlist.create('Liste de test', 'description')
>>> i = Item.create('Element de test', 'description', w)
>>>
>>> i.wishlist
<Wishlist: Wishlist object>
>>>
>>> w.items.all()
[<Item: Item object>]
----
>>> from wish.models import Wishlist, Item
>>> w = Wishlist('Liste de test', 'description')
>>> w = Wishlist.create('Liste de test', 'description')
>>> i = Item.create('Element de test', 'description', w)
>>>
>>> i.wishlist
<Wishlist: Wishlist object>
>>>
>>> w.items.all()
[<Item: Item object>]
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'attribut `related_name`.
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'attribut ``related_name``.
Querysets & managers
--------------------
* http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm
* https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator
* http://blog.etianen.com/blog/2013/06/08/django-querysets/
=== Querysets & managers
* http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm
* https://docs.djangoproject.com/en/1.9/ref/models/querysets/#django.db.models.query.QuerySet.iterator
* http://blog.etianen.com/blog/2013/06/08/django-querysets/

View File

@ -1,9 +1,7 @@
**********
Métamodèle
**********
=== Métamodèle
Sous ce titre franchement pompeux, on va un peu parler de la modélisation du modèle. Quand on prend une classe (par exemple, ``Wishlist`` que l'on a défini ci-dessus), on voit qu'elle hérite par défaut de ``models.Model``. On peut regarder les propriétés définies dans cette classe en analysant le fichier ``lib\site-packages\django\models\base.py``. On y voit notamment que ``models.Model`` hérite de ``ModelBase`` au travers de `six <https://pypi.python.org/pypi/six>`_ pour la rétrocompatibilité vers Python 2.7.
Sous ce titre franchement pompeux, on va un peu parler de la modélisation du modèle. Quand on prend une classe (par exemple, `Wishlist` que l'on a défini ci-dessus), on voit qu'elle hérite par défaut de `models.Model`. On peut regarder les propriétés définies dans cette classe en analysant le fichier `lib\site-packages\django\models\base.py`. On y voit notamment que `models.Model` hérite de `ModelBase` au travers de `six <https://pypi.python.org/pypi/six>`_ pour la rétrocompatibilité vers Python 2.7.
Cet héritage apporte notamment les fonctions ``save()``, ``clean()``, ``delete()``, ... Bref, toutes les méthodes qui font qu'une instance est sait **comment** interagir avec la base de données. La base d'un `ORM <https://en.wikipedia.org/wiki/Object-relational_mapping>`_, en fait.
Cet héritage apporte notamment les fonctions `save()`, `clean()`, `delete()`, ... Bref, toutes les méthodes qui font qu'une instance est sait **comment** interagir avec la base de données. La base d'un `ORM <https://en.wikipedia.org/wiki/Object-relational_mapping>`_, en fait.
D'autre part, chaque classe héritant de ``models.Model`` possède une propriété ``objects``. Comme on l'a vu dans la section **Jouons un peu avec la console**, cette propriété permet d'accéder aux objects persistants dans la base de données.
D'autre part, chaque classe héritant de `models.Model` possède une propriété `objects`. Comme on l'a vu dans la section **Jouons un peu avec la console**, cette propriété permet d'accéder aux objects persistants dans la base de données.

View File

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

View File

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

View File

@ -1,13 +1,6 @@
===============
Tests unitaires
===============
== Tests unitaires
*************
Méthodologies
*************
Pourquoi s'ennuyer à écrire des tests?
======================================
=== Pourquoi s'ennuyer à écrire des tests?
Traduit grossièrement depuis un article sur `https://medium.com <https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.kfyvxyb21>`_:
@ -22,153 +15,151 @@ Traduit grossièrement depuis un article sur `https://medium.com <https://medium
5.
Why Bother with Test Discipline?
================================
=== Why Bother with Test Discipline?
Your tests are your first and best line of defense against software defects. Your tests are more important than linting & static analysis (which can only find a subclass of errors, not problems with your actual program logic). Tests are as important as the implementation itself (all that matters is that the code meets the requirementhow its implemented doesnt matter at all unless its implemented poorly).
Unit tests combine many features that make them your secret weapon to application success:
1. Design aid: Writing tests first gives you a clearer perspective on the ideal API design.
2. Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.
3. Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?
4. Quality Assurance: Manual QA is error prone. In my experience, its impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features.
5. Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.
1. Design aid: Writing tests first gives you a clearer perspective on the ideal API design.
2. Feature documentation (for developers): Test descriptions enshrine in code every implemented feature requirement.
3. Test your developer understanding: Does the developer understand the problem enough to articulate in code all critical component requirements?
4. Quality Assurance: Manual QA is error prone. In my experience, its impossible for a developer to remember all features that need testing after making a change to refactor, add new features, or remove features.
5. Continuous Delivery Aid: Automated QA affords the opportunity to automatically prevent broken builds from being deployed to production.
Unit tests dont need to be twisted or manipulated to serve all of those broad-ranging goals. Rather, it is in the essential nature of a unit test to satisfy all of those needs. These benefits are all side-effects of a well-written test suite with good coverage.
“What are you testing?”
===========================================
=== What are you testing?
1. What component aspect are you testing?
2. What should the feature do? What specific behavior requirement are you testing?
1. What component aspect are you testing?
2. What should the feature do? What specific behavior requirement are you testing?
Couverture de code
==================
=== Couverture de code
On a vu au chapitre 1 qu'il était possible d'obtenir une couverture de code, c'est-à-dire un pourcentage.
Comment tester ?
================
=== Comment tester ?
Il y a deux manières d'écrire les tests: soit avant, soit après l'implémentation. Oui, idéalement, les tests doivent être écrits à l'avance. Entre nous, on ne va pas râler si vous faites l'inverse, l'important étant que vous le fassiez. Une bonne métrique pour vérifier l'avancement des tests est la couverture de code.
Pour l'exemple, nous allons écrire la fonction ``percentage_of_completion`` sur la classe ``Wish``, et nous allons spécifier les résultats attendus avant même d'implémenter son contenu. Prenons le cas où nous écrivons la méthode avant son test:
Pour l'exemple, nous allons écrire la fonction `percentage_of_completion` sur la classe `Wish`, et nous allons spécifier les résultats attendus avant même d'implémenter son contenu. Prenons le cas où nous écrivons la méthode avant son test:
.. code-block:: python
[source,python]
----
class Wish(models.Model):
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
@property
def percentage_of_completion(self):
"""
Calcule le pourcentage de complétion pour un élément.
"""
number_of_linked_parts = WishPart.objects.filter(wish=self).count()
total = self.number_of_parts * self.numbers_available
percentage = (number_of_linked_parts / total)
return percentage * 100
----
Lancez maintenant la couverture de code. Vous obtiendrez ceci:
.. code-block:: shell
[source,text]
----
$ coverage run --source "." src/manage.py test wish
$ coverage report
$ 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%
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 36 5 0 0 88%
------------------------------------------------------------------
TOTAL 69 5 4 1 93%
----
Si vous générez le rapport HTML avec la commande ``coverage html`` et que vous ouvrez le fichier ``coverage_html_report/src_wish_models_py.html``, vous verrez que les méthodes en rouge ne sont pas testées.
Si vous générez le rapport HTML avec la commande `coverage html` et que vous ouvrez le fichier `coverage_html_report/src_wish_models_py.html`, vous verrez que les méthodes en rouge ne sont pas testées.
*A contrario*, la couverture de code atteignait **98%** avant l'ajout de cette nouvelle méthode.
Pour cela, on va utiliser un fichier ``tests.py`` dans notre application ``wish``. *A priori*, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application.
Pour cela, on va utiliser un fichier `tests.py` dans notre application `wish`. *A priori*, ce fichier est créé automatiquement lorsque vous initialisez une nouvelle application.
.. code-block:: shell
[source,python]
----
from django.test import TestCase
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)
----
class TestWishModel(TestCase):
def test_percentage_of_completion(self):
"""
Vérifie que le pourcentage de complétion d'un souhait
est correctement calculé.
Sur base d'un souhait, on crée quatre parts et on vérifie
que les valeurs s'étalent correctement sur 25%, 50%, 75% et 100%.
"""
wishlist = Wishlist(name='Fake WishList',
description='This is a faked wishlist')
wishlist.save()
wish = Wish(wishlist=wishlist,
name='Fake Wish',
description='This is a faked wish',
number_of_parts=4)
wish.save()
part1 = WishPart(wish=wish, comment='part1')
part1.save()
self.assertEqual(25, wish.percentage_of_completion)
part2 = WishPart(wish=wish, comment='part2')
part2.save()
self.assertEqual(50, wish.percentage_of_completion)
part3 = WishPart(wish=wish, comment='part3')
part3.save()
self.assertEqual(75, wish.percentage_of_completion)
part4 = WishPart(wish=wish, comment='part4')
part4.save()
self.assertEqual(100, wish.percentage_of_completion)
L'attribut ``@property`` sur la méthode ``percentage_of_completion()`` va nous permettre d'appeler directement la méthode ``percentage_of_completion()`` comme s'il s'agissait d'une propriété de la classe, au même titre que les champs ``number_of_parts`` ou ``numbers_available``. Attention que ce type de méthode contactera la base de données à chaque fois qu'elle sera appelée. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de comportement a très peu d'impacts, mais ce n'est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de **cache**, que nous aborderons plus loin.
L'attribut `@property` sur la méthode `percentage_of_completion()` va nous permettre d'appeler directement la méthode `percentage_of_completion()` comme s'il s'agissait d'une propriété de la classe, au même titre que les champs `number_of_parts` ou `numbers_available`. Attention que ce type de méthode contactera la base de données à chaque fois qu'elle sera appelée. Il convient de ne pas surcharger ces méthodes de connexions à la base: sur de petites applications, ce type de comportement a très peu d'impacts, mais ce n'est plus le cas sur de grosses applications ou sur des méthodes fréquemment appelées. Il convient alors de passer par un mécanisme de **cache**, que nous aborderons plus loin.
En relançant la couverture de code, on voit à présent que nous arrivons à 99%:
.. code-block:: shell
[source,shell]
----
$ coverage run --source='.' src/manage.py test wish; coverage report; coverage html;
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
$ 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%
OK
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Name Stmts Miss Branch BrPart Cover
------------------------------------------------------------------
src\gwift\__init__.py 0 0 0 0 100%
src\gwift\settings\__init__.py 4 0 0 0 100%
src\gwift\settings\base.py 14 0 0 0 100%
src\gwift\settings\dev.py 8 0 2 0 100%
src\manage.py 6 0 2 1 88%
src\wish\__init__.py 0 0 0 0 100%
src\wish\admin.py 1 0 0 0 100%
src\wish\models.py 34 0 0 0 100%
src\wish\tests.py 20 0 0 0 100%
------------------------------------------------------------------
TOTAL 87 0 4 1 99%
----
En continuant de cette manière (ie. Ecriture du code et des tests, vérification de la couverture de code), on se fixe un objectif idéal dès le début du projet. En prenant un développement en cours de route, fixez-vous comme objectif de ne jamais faire baisser la couverture de code.
*********************
Quelques liens utiles
*********************
=== Quelques liens utiles
* `Django factory boy <https://github.com/rbarrois/django-factory_boy/tree/v1.0.0>`_
* `Django factory boy <https://github.com/rbarrois/django-factory_boy/tree/v1.0.0>`_

View File

@ -86,7 +86,7 @@ include::django/admin.adoc[]
Pour commencer, nous allons nous concentrer sur la création d'un site ne contenant qu'une seule application, même si en pratique le site contiendra déjà plusieurs applications fournies pas django, comme nous le verrons plus loin.
Pour prendre un exemple concret, nous allons créer un site permettant de gérer des listes de souhaits, que nous appellerons ``gwift`` (pour ``GiFTs and WIshlisTs`` :)).
Pour prendre un exemple concret, nous allons créer un site permettant de gérer des listes de souhaits, que nous appellerons `gwift` (pour `GiFTs and WIshlisTs` :)).
La première chose à faire est de définir nos besoins du point de vue de l'utilisateur, c'est-à-dire ce que nous souhaitons qu'un utilisateur puisse faire avec l'application.

File diff suppressed because it is too large Load Diff

View File

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