Utilisation de mixins avec les vues fondées sur les classes¶
Prudence
This is an advanced topic. A working knowledge of Django’s class-based views is advised before exploring these techniques.
Les vues fondées sur les classes intégrées dans Django fournissent de nombreuses fonctionnalités, mais certaines d’entre elles pourraient être utiles de manière indépendante. Par exemple, si l’on écrit une vue qui utilise un gabarit pour produire une réponse HTTP, mais sans utiliser TemplateView; ou encore si l’on veut utiliser un gabarit seulement pour une requête POST et faire quelque chose de totalement différent pour les requêtes GET. Même si on pourrait utiliser directement TemplateResponse, on se retrouverait avec pas mal de code dupliqué.
C’est pour cette raison que Django fournit aussi un certain nombre de mixins englobant des fonctionnalités de manière distincte. Le rendu de gabarits, par exemple, est isolé dans la classe TemplateResponseMixin. La documentation de référence de Django contient une documentation complète de toutes les classes mixins.
Contexte et réponses par gabarit¶
Deux classes mixins centrales sont à disposition pour aider à présenter une interface cohérente pour travailler avec les gabarits dans les vues fondées sur les classes.
TemplateResponseMixinEvery built in view which returns a
TemplateResponsewill call therender_to_response()method thatTemplateResponseMixinprovides. Most of the time this will be called for you (for instance, it is called by theget()method implemented by bothTemplateViewandDetailView); similarly, it’s unlikely that you’ll need to override it, although if you want your response to return something not rendered via a Django template then you’ll want to do it. For an example of this, see the JSONResponseMixin example.render_to_response()appelle elle-mêmeget_template_names()qui consulte l’attributtemplate_namede la vue, par défaut. Deux autres mixins (SingleObjectTemplateResponseMixinetMultipleObjectTemplateResponseMixin) surchargent cette méthode pour offrir des valeurs par défaut plus souples lorsqu’il s’agit de manipuler des objets réels.ContextMixinEvery built in view which needs context data, such as for rendering a template (including
TemplateResponseMixinabove), should callget_context_data()passing any data they want to ensure is in there as keyword arguments.get_context_data()returns a dictionary; inContextMixinit returns its keyword arguments, but it is common to override this to add more members to the dictionary. You can also use theextra_contextattribute.
Construction des vues fondées sur les classes de Django¶
Voyons comment deux des vues fondées sur les classes de Django sont construites à partir de mixins fournissant des fonctionnalités isolées. Nous examinerons DetailView qui produit une vue de détail d’un objet et ListView qui produit une liste d’objets, généralement à partir d’un jeu de requête, avec pagination facultative. Ceci nous amènera à étudier quatre mixins qui, combinées entre elles, fournissent des fonctionnalités utiles lors du traitement d’un objet Django unique ou d’un ensemble d’objets.
Il existe également des classes mixins dans les vues génériques d’édition (FormView et les vues spécifiques aux modèles CreateView, UpdateView et DeleteView), ainsi que dans les vues génériques centrées sur les dates. Ces classes sont décrites dans la documentation de référence des mixins.
DetailView: traitement d’un seul objet Django¶
Pour afficher le détail d’un objet, il y a fondamentalement deux choses à faire : récupérer l’objet, puis produire une réponse TemplateResponse à l’aide du gabarit adéquat, en transmettant l’objet dans le contexte.
To get the object, DetailView
relies on SingleObjectMixin,
which provides a
get_object()
method that figures out the object based on the URL of the request (it
looks for pk and slug keyword arguments as declared in the
URLConf, and looks the object up either from the
model attribute
on the view, or the
queryset
attribute if that’s provided). SingleObjectMixin also overrides
get_context_data(),
which is used across all Django’s built in class-based views to supply
context data for template renders.
To then make a TemplateResponse,
DetailView uses
SingleObjectTemplateResponseMixin, which
extends TemplateResponseMixin, overriding
get_template_names() as
discussed above. It actually provides a fairly sophisticated set of options,
but the main one that most people are going to use is
<app_label>/<model_name>_detail.html. The _detail part can be changed
by setting
template_name_suffix
on a subclass to something else. (For instance, the generic edit views use _form for create and
update views, and _confirm_delete for delete views.)
ListView: traitement de plusieurs objets Django¶
Les listes d’objets suivent grosso modo le même modèle : récupérer d’abord une liste d’objets (potentiellement paginés), typiquement un objet QuerySet, puis produire une réponse TemplateResponse avec le gabarit adéquat pour exploiter cette liste d’objets.
Pour obtenir les objets, ListView utilise MultipleObjectMixin qui offre à la fois get_queryset() et paginate_queryset(). Au contraire de SingleObjectMixin, il n’est pas nécessaire d’identifier des éléments d’URL pour déterminer le jeu de requête à traiter ; le comportement par défaut est d’utiliser l’attribut queryset ou model de la classe de vue. Une raison fréquente de vouloir surcharger ici get_queryset() est d’adapter la liste d’objets de manière dynamique, par exemple en fonction de l’utilisateur actuel ou pour exclure des articles dont la date est dans le futur dans un blog.
MultipleObjectMixin also overrides
get_context_data() to
include appropriate context variables for pagination (providing
dummies if pagination is disabled). It relies on object_list being
passed in as a keyword argument, which ListView arranges for
it.
Pour produire une réponse TemplateResponse, ListView utilise ensuite MultipleObjectTemplateResponseMixin; comme pour SingleObjectTemplateResponseMixin ci-dessus, get_template_names() est surchargée pour fournir une série d'options dont la plus utilisée est le nom de gabarit <étiquette_application>/<nom_modèle>_list.html où la partie _list provient aussi de l’attribut template_name_suffix (les vues génériques centrées sur les dates utilisent des suffixes tels que _archive, _archive_year, etc. pour utiliser différents gabarits correspondant aux différentes vues de listes spécialisées pour les dates).
Utilisation des mixins de vues fondées sur les classes de Django¶
Maintenant que nous avons vu comment les classes génériques fondées sur les classes de Django utilisent les mixins à disposition, examinons d’autres façons de les combiner. Nous allons toujours les combiner avec d’autres vues intégrées ou génériques fondées sur les classes, mais les cas d’utilisation auxquels Django répond ne recouvrent pas toutes les situations plus rares auxquelles on peut être confronté.
Avertissement
On ne peut pas combiner tous les mixins, ni combiner n’importe quel mixin avec toutes les vues génériques fondées sur les classes. Nous présentons ici quelques exemples qui fonctionnent ; si vous souhaitez construire d’autres fonctionnalités, vous devrez étudier les interactions entre les attributs et les méthodes qui se chevauchent entre les différentes classes utilisées, ainsi que la façon dont l’ordre de résolution des méthodes affecte la version des méthodes appelée et l’ordre de ces appels.
La documentation de référence des vues fondées sur les classes et des mixins de vues fondées sur les classes de Django vous aidera à comprendre quels attributs et méthodes peuvent être la source de conflits entre les différentes classes et mixins.
Dans le doute, il est souvent préférable de simplifier les choses en se basant sur View ou TemplateView, peut-être avec SingleObjectMixin ou MultipleObjectMixin. Même si vous devez finalement écrire un peu plus de code, il sera probablement plus compréhensible pour quelqu’un devant s’y atteler plus tard, et en réduisant les interactions à surveiller, vous vous économiserez un peu de temps de réflexion (il est bien sûr toujours possible de se plonger dans l’implémentation de Django des vues génériques fondées sur les classes pour y chercher de l’inspiration sur la façon de traiter les problèmes).
Utilisation de SingleObjectMixin avec View¶
Si nous voulons écrire une classe de vue ne répondant qu’aux requêtes POST, nous héritons de View et écrivons une méthode post() dans la sous-classe. Cependant, si le traitement doit porter sur un objet particulier identifié par l’URL, il est souhaitable de profiter de la fonctionnalité offerte par SingleObjectMixin.
We’ll demonstrate this with the Author model we used in the generic
class-based views introduction.
views.py¶from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author
class RecordInterestView(SingleObjectMixin, View):
"""Records the current user's interest in an author."""
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
# Look up the author we're interested in.
self.object = self.get_object()
# Actually record interest somehow here!
return HttpResponseRedirect(
reverse("author-detail", kwargs={"pk": self.object.pk})
)
En pratique, l’intérêt devrait probablement être enregistré dans un stockage clé-valeur plutôt que dans une base de données relationnelle, nous avons donc laissé cet aspect de côté. La seule partie de la vue qui peut profiter de l’utilisation de SingleObjectMixin est l’endroit où il s’agit de récupérer l’auteur concerné par l’intérêt, ce qui est fait en appelant self.get_object(). Tout le reste est pris en charge pour nous par le mixin.
Nous pouvons assez facilement brancher cette vue dans nos URL :
urls.py¶from django.urls import path
from books.views import RecordInterestView
urlpatterns = [
# ...
path(
"author/<int:pk>/interest/",
RecordInterestView.as_view(),
name="author-interest",
),
]
Remarquez le groupe nommé pk qui sera utilisé par get_object() pour rechercher l’instance Author. Il est aussi possible d’utiliser un « slug » ou toute autre fonctionnalité de SingleObjectMixin.
Utilisation de SingleObjectMixin avec ListView¶
ListView intègre la pagination, mais il peut être par exemple souhatiable de paginer une liste d’objets qui sont tous liés (par clé étrangère) à un autre objet. Dans notre exemple de publication, on pourrait vouloir paginer tous les livres d’un éditeur particulier.
Une façon de faire cela serait de combiner ListView avec SingleObjectMixin, afin que le jeu de requête de la liste de livres paginée puisse dépendre de l’éditeur trouvé comme objet unique. Afin de faire cela, il est nécessaire d’avoir deux jeux de requête différents :
- Le jeu de requête
Bookutilisé parListView Comme nous avons accès à l’éditeur
Publisherdes livres que nous souhaitons afficher, nous surchargeonsget_queryset()et utilisons le gestionnaire inverse de clé étrangère de l’objetPublisher.Publisherqueryset for use inget_object()Nous comptons sur l’implémentation par défaut de
get_object()pour récupérer le bon objetPublisher. Cependant, nous devons explicitement transmettre un paramètrequerysetcar sinon, l’implémentation par défaut deget_object()appelleraitget_queryset()que nous avons surchargée pour renvoyer des objetsBookau lieu d’objetsPublisher.
Note
Il faut être prudent avec get_context_data(). Comme les deux classes SingleObjectMixin et ListView vont placer des éléments dans les données de contexte sous la valeur context_object_name, si elle est définie, nous allons plutôt nous assurer de manière explicite que l’objet Publisher se trouve bien dans les données de contexte. ListView se chargera d’ajouter les contenus page_obj et paginator adéquats pour autant que nous n’oubliions pas d’appeler super().
Nous pouvons maintenant écrire une nouvelle vue PublisherDetailView:
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher
class PublisherDetailView(SingleObjectMixin, ListView):
paginate_by = 2
template_name = "books/publisher_detail.html"
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=Publisher.objects.all())
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["publisher"] = self.object
return context
def get_queryset(self):
return self.object.book_set.all()
Remarquez notre manière de définir self.object dans get() afin que nous puissions ensuite l’utiliser dans get_context_data() et get_queryset(). Si vous ne définissez pas template_name, le nom du gabarit prendra la valeur par défaut choisie par ListView, ce qui donnera dans ce cas "books/book_list.html" car il s’agit d’une liste de livres. ListView n’a pas conscience de la présence de SingleObjectMixin, elle ne sait donc pas du tout que cette vue a quelque chose à voir avec un objet Publisher.
Nous avons délibérément défini paginate_by à un petit nombre dans l’exemple afin que vous n’ayez pas à créer beaucoup de livres pour voir la pagination à l’œuvre ! Voici le gabarit que vous pourriez utiliser :
{% extends "base.html" %}
{% block content %}
<h2>Publisher {{ publisher.name }}</h2>
<ol>
{% for book in page_obj %}
<li>{{ book.title }}</li>
{% endfor %}
</ol>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endblock %}
Risque de trop grande complexité¶
Généralement, il est possible d’utiliser TemplateResponseMixin et SingleObjectMixin lorsque leurs fonctionnalités sont nécessaires. Comme montré plus haut, en prenant certaines précautions, on peut même combiner SingleObjectMixin avec ListView. Cependant, les choses deviennent de plus en plus complexes avec ces combinaisons ; voici une bonne règle générale :
Indication
Each of your views should use only mixins or views from one of the groups
of generic class-based views: detail, list, editing and date. For example it’s
fine to combine TemplateView (built in view) with
MultipleObjectMixin (generic list), but
you’re likely to have problems combining SingleObjectMixin (generic
detail) with MultipleObjectMixin (generic list).
Pour montrer ce qui arrive quand on essaie de trop compliquer, nous présentons un exemple qui sacrifie la lisibilité et la maintenabilité alors qu’il existe une solution plus simple. Examinons d’abord une tentative naïve de combiner DetailView avec FormMixin pour permettre l’envoi POST par un formulaire Form Django vers la même URL qui est utilisée pour afficher un objet avec DetailView.
Utilisation de FormMixin avec DetailView¶
Rappelez-vous notre exemple précédent qui combinait View et SingleObjectMixin. Nous enregistrions l’intérêt d’un utilisateur pour un auteur particulier. Admettons que nous voulions maintenant donner la possibilité d’écrire un message motivant leur intérêt. Nous partons encore une fois du principe que nous ne stockerons pas cela dans une base de données relationnelle, mais plutôt dans quelque chose de plus exotique dont nous ne nous soucierons pas dans cet exemple.
À ce stade, il est naturel de faire appel à un formulaire Form pour englober les informations envoyées par le navigateur de l’utilisateur vers Django. En admettant que nous adhérons aussi aux principes de REST, nous aimerions utiliser la même URL pour l’affichage de l’auteur que pour la capture du message en provenance de l’utilisateur. Réécrivons notre vue AuthorDetailView dans cette optique.
Nous conserverons le traitement GET de la classe DetailView, même s’il faudra rajouter un formulaire Form dans les données de contexte pour pouvoir l’afficher dans le gabarit. Nous allons aussi profiter du traitement de formulaire de FormMixin et écrire un peu de code afin qu’en cas d’envoi POST, le formulaire soit instancié de manière appropriée.
Note
Nous utilisons FormMixin et implémentons nous-même la méthode post() plutôt que d’essayer de mélanger DetailView avec FormView (qui fournit déjà une méthode post() exploitable), parce que ces deux vues implémentent get() et que cela risquerait d’ajouter de la confusion.
Voici à quoi ressemble notre nouvelle vue AuthorDetailView:
# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.
from django import forms
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(FormMixin, DetailView):
model = Author
form_class = AuthorInterestForm
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
# Here, we would record the user's interest using the message
# passed in form.cleaned_data['message']
return super().form_valid(form)
get_success_url() sert à indiquer une destination de redirection, ce qui est utilisé dans l’implémentation par défaut de form_valid(). Nous devons fournir notre propre méthode post(), comme expliqué précédemment.
Une meilleure solution¶
Le nombre d’interactions subtiles entre FormMixin et DetailView éprouve déjà nos capacités conceptuelles. Il est improbable que vous ayez envie d’écrire vous-même ce genre de classe.
Dans ce cas, vous pourriez écrire vous-même la méthode post(), en conservant DetailView comme seule fonctionnalité générique, même si l’écriture du code de gestion des formulaires implique beaucoup de duplication.
Sinon, une approche certainement plus économique que ci-dessus serait d’avoir une vue séparée pour le traitement du formulaire, ce qui permettrait d’utiliser sans problème FormView séparément de DetailView.
Une meilleure solution alternative¶
En réalité, ce que nous essayons de faire ici est d’utiliser deux vues classes différentes pour la même URL. Pourquoi donc ne pas simplement faire cela ? La division est ici très claire : les requêtes GET devraient aboutir à la vue DetailView (avec en plus le formulaire dans les données de contexte) et les requêtes POST à la vue FormView. Mettons d’abord en place ces deux vues.
La vue AuthorDetailView est presque la même que celle que nous avons présentée en introduction. Nous devons écrire notre propre méthode get_context_data() pour mettre à disposition AuthorInterestForm dans le gabarit. Nous omettons la surcharge de get_object() de la version précédente par souci de clarté :
from django import forms
from django.views.generic import DetailView
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(DetailView):
model = Author
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = AuthorInterestForm()
return context
Puis pour AuthorInterestFormView, il s’agit d’une sous-classe de FormView, mais nous devons y adjoindre SingleObjectMixin pour pouvoir obtenir l’auteur concerné par l’action en cours et ne pas oublier de définir template_name pour s’assurer que les erreurs de formulaires soient affichées dans le même gabarit que AuthorDetailView utilise lors de la requête GET:
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
class AuthorInterestFormView(SingleObjectMixin, FormView):
template_name = "books/author_detail.html"
form_class = AuthorInterestForm
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
Finally we bring this together in a new AuthorView view. We
already know that calling as_view() on
a class-based view gives us something that behaves exactly like a function
based view, so we can do that at the point we choose between the two subviews.
You can pass through keyword arguments to
as_view() in the same way you
would in your URLconf, such as if you wanted the AuthorInterestFormView
behavior to also appear at another URL but using a different template:
from django.views import View
class AuthorView(View):
def get(self, request, *args, **kwargs):
view = AuthorDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = AuthorInterestFormView.as_view()
return view(request, *args, **kwargs)
Cette approche peut également être employée avec toute autre vue fondée sur les classes, générique ou créée par vos soins, héritant directement de View ou de TemplateView, car elle conserve les différentes vues aussi distinctes que possible.
Plus que du simple code HTML¶
Les vues fondées sur les classes se révèlent particulièrement utiles au moment où l’on a besoin de répéter plusieurs fois un processus semblable. Imaginez qu’il faille écrire une API et que chaque vue doive renvoyer du JSON au lieu de produire une page HTML.
Il est alors possible de créer une classe mixin afin de l’utiliser dans chaque vue, gérant ainsi à un seul endroit la conversion vers le format JSON.
Par exemple, une classe mixin pour JSON pourrait ressembler à ceci :
from django.http import JsonResponse
class JSONResponseMixin:
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(self.get_data(context), **response_kwargs)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
Note
Consultez la documentation Sérialisation d’objets Django pour plus d’informations sur la façon de transformer correctement des modèles Django et des jeux de requête au format JSON.
This mixin provides a render_to_json_response() method with the same
signature as
render_to_response().
To use it, we need to mix it into a TemplateView for example, and override
render_to_response() to call render_to_json_response() instead:
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
Il serait aussi possible d’utiliser cette classe mixin avec l’une des vues génériques. On peut composer sa propre version de DetailView en combinant JSONResponseMixin avec BaseDetailView (celle-ci contenant le comportement de DetailView avant le rendu du gabarit) :
from django.views.generic.detail import BaseDetailView
class JSONDetailView(JSONResponseMixin, BaseDetailView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
Cette vue peut ensuite être déployée de la même façon que pour n’importe quelle vue DetailView, reproduisant le même comportement à l’exception du format de la réponse.
If you want to be really adventurous, you could even mix a
DetailView subclass that is able
to return both HTML and JSON content, depending on some property of
the HTTP request, such as a query argument or an HTTP header. Mix in both the
JSONResponseMixin and a
SingleObjectTemplateResponseMixin,
and override the implementation of
render_to_response()
to defer to the appropriate rendering method depending on the type of response
that the user requested:
from django.views.generic.detail import SingleObjectTemplateResponseMixin
class HybridDetailView(
JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView
):
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get("format") == "json":
return self.render_to_json_response(context)
else:
return super().render_to_response(context)
Because of the way that Python resolves method overloading, the call to
super().render_to_response(context) ends up calling the
render_to_response()
implementation of TemplateResponseMixin.