Vues
Fonctions ou classes
Une vue peut s’écrire soit sous forme d’une fonction, soit sous forme d’une classe. L’application a déjà un exemplaire de chacune des formes : la fonction index, introduite dans le chapitre Création de site, et la classe IndexView, introduite dans le chapitre Routage.
Avoir deux façons pour un même but est inhabituel sous Django, mais l’explication est historique : à l’origine, les vues devaient être implémentées par des fonctions. Au fil du temps, ce schéma a montré ses limites : beaucoup de logique conditionnelle pour traiter les variations (les verbes HTTP par exemple) ; code trop verbeux ; difficulté à factoriser d’où des répétitions ou, pour les éviter, une organisation du code confuse. Une autre architecture, fondée sur des classes, a été introduite en version 1.3. L’intention n’est pas de prendre la place de l’écriture en fonctions, mais d’offrir une alternative avec des avantages. Ce changement de paradigme a pu être vécu comme une épreuve pour des développeurs habitués depuis longtemps à garder une totale visibilité du fonctionnement à travers leurs fonctions. En effet, avec des classes, on raisonne plutôt en assemblage et imbrication...
Vues intégrées
1. Vues de base
La section précédente a introduit la classe View en tant que base des vues. Sur cette base, d’autres vues sont mises à disposition pour satisfaire les besoins classiquement rencontrés. Ainsi, la vue RedirectView a déjà été utilisée dans le chapitre Routage.
La vue TemplateView est probablement la plus élémentaire, puisqu’il s’agit de faire le rendu d’un gabarit. La vue index(), introduite dans le chapitre Création de site au plus simple, c’est-à-dire sous forme de fonction dans le fichier main\views.py, peut être éliminée en la remplaçant par une vue générique, directement citée dans le fichier de routage :
mysite\urls.py
[...]
from django.views.generic import TemplateView
#from mysite.main import views
urlpatterns = [
[...]
# path('', views.index), # remplacé par :
path('', TemplateView.as_view(template_name='index.html')),
[...]
Les paramètres dans l’appel à as_view() sont passés au constructeur de la vue et celui-ci les traite comme des affectations de valeurs aux attributs de l’instance. Ce mécanisme permet donc de personnaliser à son goût...
Greffons
Les vues génériques ont une valeur appréciable en mettant à disposition une implémentation déjà écrite pour des traitements récurrents. Elles ont un premier degré de personnalisation, facile d’accès à travers le positionnement d’attributs. Un exemple déjà vu est l’attribut template_name de la vue TemplateView. Malgré tout, il arrive qu’on ait besoin d’une vue très similaire à une vue générique et pourtant aucune ne colle parfaitement au besoin. Il serait dommage d’avoir à tout écrire depuis la base View. Heureusement, les vues sont conçues sur une architecture modulaire, qui privilégie l’assemblage de pièces élémentaires. L’héritage multiple de classes, possible en langage Python, est le principe qui permet cette construction. Une vue se construit sur la base d’une autre vue, avec des enrichissements apportés par héritage complémentaire d’une ou plusieurs classes auxiliaires. Cet auxiliaire est désigné par la documentation Django (anglaise et française) sous le terme mixin. Dans cet ouvrage, il est adopté une traduction par le terme français greffon.
Par exemple, la vue TemplateView est construite ainsi :
class TemplateView(TemplateResponseMixin, ContextMixin...
Données de contexte
Dans le processus de rendu d’une page, l’objet des données de contexte est un support de passation de données : le moteur de gabarit instancie la page en remplaçant les variables mentionnées dans le gabarit par les valeurs piochées dans l’objet des données de contexte, lui-même ayant été alimenté au préalable par la vue.
Dans les usages ordinaires des vues, un objet des données de contexte est un simple dictionnaire primitif et il est désigné dans la documentation par le mot anglais context, avec un caractère minuscule. Il ne faut pas le confondre avec le mot Context, écrit avec un caractère majuscule, qui désigne une classe du module django.template. Cette classe et sa sous-classe RequestContext sont des objets employés au niveau des interfaces de plus bas niveau, avec les moteurs de rendu.
Si on reprend l’exemple précédent de TemplateView, l’objet des données de contexte est initialisé avec les éventuels paramètres extraits de l’URL. Son enrichissement est ensuite sous-traité au greffon ContextMixin par sa méthode get_context_data(). Celui-ci fait deux compléments : il ajoute l’objet vue sous la clé view et il actualise le dictionnaire avec le contenu de l’éventuel paramètre...
Processeurs de contexte
Les processeurs de contexte ont été effleurés dans le chapitre Création de site, juste à l’occasion de l’exploration de la configuration, et il n’était question à ce stade que des processeurs intégrés disponibles.
Le rôle de ces composants va être illustré avec l’écriture d’un processeur personnalisé pour l’application.
Un processeur de contexte est une entité appelable destinée à alimenter l’objet de données de contexte. Il revêt un caractère systématique par son activation au niveau de la configuration et sa sollicitation à chaque rendu d’un gabarit.
Son contrat d’interface est de recevoir un objet requête et de retourner un dictionnaire, éventuellement vide, d’entrées qui seront mises à disposition dans le contexte. Traditionnellement, un processeur s’écrit sous la forme d’une fonction, au plus simple.
Pour une application de messagerie, il est habituel d’informer spontanément l’utilisateur du nombre de ses messages non lus et cette information a des chances d’être présentée sur toutes les pages, par exemple dans un bandeau en en-tête. Plutôt que de devoir répéter l’ajout au contexte de cette valeur pour chacune des vues, il est plus efficace de mettre en place un processeur de contexte qui fera le travail une fois pour toutes.
Le composant peut être logé n’importe où, mais il est conventionnel d’employer un module de nom context_processors.py sous la racine de l’application.
Créez le fichier des processeurs de contexte :
mysite\messenger\context_processors.py
from .models import Message
def inbox(request):
if request.user.is_authenticated:
return {'messenger_unread_count': Message.objects.filter(
recipient=request.user,
read_at__isnull=True,
recipient_deleted_at__isnull=True).count()
}
else:
...
Requêtes AJAX
L’infrastructure logicielle traite les requêtes de nature AJAX avec les mêmes mécanismes qu’une requête ordinaire. La distinction est facilitée par une méthode sur l’objet requête, pour rendre un simple booléen. Le traitement de la requête se fait avec une vue, comme d’habitude, sauf que très souvent la réponse est au format JSON plutôt que HTML.
Pour une application de messagerie, on peut imaginer un besoin de maintenir actuelle une pièce d’information dans la page du navigateur, autrement que par des rechargements complets de la page, automatiquement ou à l’initiative de l’utilisateur. Un exemple typique est un compteur de messages non lus naturellement à jour. Diverses techniques, plus ou moins réactives et laborieuses, existent pour remplir la fonctionnalité. Par nature, la messagerie n’a pas vocation à être hautement réactive, donc une technique par interrogation régulière à basse fréquence est suffisante dans le cas présent.
Supposons que la page soit pourvue d’un code pour interroger en AJAX le serveur toutes les N minutes afin d’obtenir l’état actuel du nombre de messages non lus dans la boîte d’arrivée de l’utilisateur.
Pour commencer, une route est établie :
mysite\messenger\urls.py
[...]
from .views import index, IndexView
from .views import AjaxUnreadCountView # démo AJAX
[...]
urlpatterns = [
[...]
# démo AJAX
path('unread-count/', AjaxUnreadCountView.as_view()),
[...]
Une ligne isolée est utilisée pour l’import de la vue, pour être plus facilement retirée ou désactivée par une mise en commentaire, si cette route n’est pas conservée après la démonstration.
Le nom de la vue est volontairement préfixé avec le motif Ajax, par commodité de lecture, pour bien rappeler la nature qui lui est assignée.
Ensuite, une vue est construite :
mysite\messenger\views.py
[...]
from django.http import JsonResponse
[...]
from django.views import View
[...]
class AjaxUnreadCountView(View): ...
Intégrations en modèles
Avec les sections précédentes, il est à remarquer que le besoin d’obtenir le compte des messages non lus est apparu à deux occasions : dans un processeur de contexte pour mettre le compteur à disposition des gabarits et dans une vue AJAX pour permettre à la page de s’actualiser. Dans le chapitre consacré aux pages et gabarits, le besoin apparaît une fois de plus. Dans tous les cas, la requête est construite de la même façon. Il n’est évidemment pas question de supporter cette duplication de code, un remaniement s’impose.
Le lieu préférentiel pour la centralisation d’un traitement portant sur les instances d’un modèle est le gestionnaire de ce modèle. Un gestionnaire personnalisé a déjà été préparé dans le chapitre consacré aux modèles, mais laissé vide jusqu’à présent.
Complétez le gestionnaire de modèle :
mysite\messenger\models.py
[...]
class MessageManager(models.Manager):
def unread_count(self, user):
return self.filter(
recipient=user,
read_at__isnull=True,
recipient_deleted_at__isnull=True).count() ...
Simulation d’authentification
Pour la suite des travaux, il devient trop difficile de rester non authentifié, car il faudra cibler les messages se rapportant à un utilisateur. Forger les requêtes comme on a pu le faire jusqu’à maintenant pour faire croire à un utilisateur authentifié devient trop laborieux. Par ailleurs, l’authentification est un domaine suffisamment vaste pour mériter d’être traité à part. Pour éluder le point dans l’immédiat, de simples émulations vont être mises en place, ce qui permet d’avancer en restant concentré sur le sujet du chapitre.
Il est préférable de ne pas polluer l’application messenger avec ces considérations de travail et donc on va plutôt tirer profit de la présence de l’application main pour lui faire héberger ces besoins auxiliaires.
Tout d’abord, un point d’intendance est à régler : l’application auth s’appuie sur l’application sessions pour faire une partie du travail et doit être activée dans la configuration si ce n’est pas déjà le cas.
Activez la ligne pour sessions :
mysite\settings.py
INSTALLED_APPS = [
[...]
'django.contrib.contenttypes', # dépendance de admin
'django.contrib.sessions'...
Écritures des vues
Cette section va être consacrée à la construction des vues de l’application. Le travail de rendu effectif des pages est volontairement différé au chapitre suivant, relatif aux pages et gabarits.
Jusqu’à maintenant, le fichier de routage contient quelques routes, mais elles mènent vers une même vue index d’attente, à but démonstratif. Il est temps de les reprendre une à une pour leur donner leur vrai rôle.
1. Dossier d’arrivée
Modifiez le fichier de routage :
mysite\messenger\urls.py
[...]
from .views import index
from .views import (InboxView,
IndexView,
)
[...]
urlpatterns = [
[...]
path('inbox/', InboxView.as_view(), name='inbox'),
[...]
L’import de la vue index est isolé sur une ligne en prévision de sa future élimination, puisque les usages de cette vue transitoire vont être progressivement remplacés. L’instruction d’import des vues est préparée pour être répartie sur plusieurs lignes, qui vont se remplir au fil des adaptations.
Modifiez le fichier des vues pour créer cette nouvelle vue :
mysite\messenger\views.py
[...]
from django.contrib.auth.decorators import login_required
[...]
from django.views.generic import RedirectView, TemplateView
[...]
class InboxView(TemplateView):
http_method_names = ['get']
template_name = 'messenger/inbox.html'
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
msgs = Message.objects.filter(
recipient=self.request.user,
recipient_deleted_at__isnull=True)
...