fixing part-3-django content

This commit is contained in:
Fred 2020-04-13 15:01:36 +02:00
parent fec56cfb3d
commit fadd7c592d
13 changed files with 268 additions and 139 deletions

View File

@ -14,4 +14,4 @@ html:
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
pdf:
asciidoctor-pdf -a pdf-themesdir=resources/themes -a pdf-theme=gwift source/main.adoc -t -r asciidoctor-diagram
asciidoctor-pdf -a pdf-themesdir=resources/themes -a pdf-theme=gwift-theme source/main.adoc -t -r asciidoctor-diagram

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -2,12 +2,32 @@
Dans ce chapitre, on va parler de plusieurs concepts utiles au développement rapide d'une application. On parlera de modélisation, de migrations, d'administration auto-générée.
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.
Pour reprendre une partie du schéma précédent, on a une requête qui est émise par un utilisateur. La première étape consiste à trouver une route qui correspond à cette requête, c'est à dire à trouver la correspondance entre l'URL demandée et la fonction qui sera exécutée. Cette fonction correspond au *contrôleur* et s'occupera de construire le *modèle* correspondant.
En simplifiant, Django suit bien le modèle MVC, et toutes ces étapes sont liées ensemble grâce aux différentes routes, définies dans les fichiers `urls.py`.
include::models.adoc[]
include::admin.adoc[]
include::forms.adoc[]
include::mvc.adoc[]
include::views.adoc[]
include::templates.adoc[]
include::layout.adoc[]
include::urls.adoc[]
include::auth.adoc[]
include::logging.adoc[]
NOTE: Ne pas oublier de parler des sessions. Mais je ne sais pas si c'est le bon endroit.
include::multilingual.adoc[]
include::urls.adoc[]

View File

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

View File

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

View File

@ -1,28 +1,3 @@
Templates tags
--------------
[source,python]
----
from django.template.defaultfilters import urlize
class Suggestion(BaseModel):
"""Représentation des suggestions.
"""
created_by = models.ForeignKey(user_model, on_delete=models.DO_NOTHING, verbose_name="Créé par")
manager = models.ForeignKey(
user_model,
on_delete=models.DO_NOTHING,
verbose_name="Gestionnaire",
null=True,
blank=True,
related_name="managed_by"
)
subject = models.TextField(verbose_name="Sujet")
def urlized_subject(self):
"""
Voir https://docs.djangoproject.com/fr/3.0/howto/custom-template-tags/
"""
return urlize(self.subject, autoescape=True)
----

View File

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

View File

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

View File

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