Utilisation de mixins avec les vues fondées sur les classes

Prudence

Ceci est un sujet avancé. Nous suggérons d’avoir une bonne connaissance préalable des vues Django fondées sur les classes avant d’explorer ces 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.

TemplateResponseMixin

Chaque vue intégrée renvoyant une réponse TemplateResponse appelle la méthode render_to_response() fournie par TemplateResponseMixin. Dans la plupart des cas, cet appel se fait automatiquement (par exemple, elle est appelée par la méthode get() aussi bien de TemplateView que de DetailView). De même, il est peu probable que vous ayez besoin de la surcharger, bien que cela peut se révéler utile si vous voulez que la réponse renvoie du contenu non rendu par un gabarit Django. Vous pouvez trouver un exemple d’un tel usage dans l’exemple JSONResponseMixin.

render_to_response() appelle elle-même get_template_names() qui ne fait que consulter l’attribut template_name de la vue, par défaut. Deux autres mixins (SingleObjectTemplateResponseMixin et MultipleObjectTemplateResponseMixin) surchargent cette méthode pour offrir des valeurs par défaut plus souples lorsqu’il s’agit de manipuler des objets réels.

ContextMixin
Every built in view which needs context data, such as for rendering a template (including TemplateResponseMixin above), should call get_context_data() passing any data they want to ensure is in there as keyword arguments. get_context_data() returns a dictionary; in ContextMixin it simply returns its keyword arguments, but it is common to override this to add more members to the dictionary. You can also use the extra_context attribute.

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.

Pour obtenir l’objet, DetailView se base sur SingleObjectMixin qui fournit une méthode get_object() qui va rechercher l’objet en se basant sur l’URL de la requête (elle cherche les paramètres nommés pk et slug tels que déclarés dans la configuration d’URL et sélectionne l’objet en utilisant soit l’attribut model de la vue, soit l’attribut queryset si celui-ci est présent). SingleObjectMixin surcharge aussi get_context_data(), qui est utilisée dans toutes les vues fondées sur les classes de Django pour fournir les données de contexte servant au rendu des gabarits.

Pour produire la réponse TemplateResponse, DetailView utilise SingleObjectTemplateResponseMixin, qui étend TemplateResponseMixin, surchargeant get_template_names() comme expliqué plus haut. Il fournit en réalité un bon nombre d’options élaborées, mais le nom que la plupart des gens vont utiliser est <étiquette_application>/<nom_modèle>_detail.html. La partie _detail peut être modifiée en définissant un attribut template_name_suffix différent dans une sous-classe (par exemple, les vues génériques d’édition emploient _form pour les vues de création et de mise à jour et _confirm_delete pour les vues de suppression).

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 de simplement 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 surcharge aussi get_context_data() pour inclure les variables de contexte appropriées à la pagination (indiquant des valeurs vides si la pagination est désactivée). Elle compte sur la présence de object_list dans les paramètres nommés transmis, ce dont se charge ListView.

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 naturellement 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 simple 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.

Nous allons démontrer cela avec le modèle Author que nous avons utilisé dans l’introduction aux vues génériques basées sur les classes.

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 RecordInterest(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 simplement 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 RecordInterest

urlpatterns = [
    #...
    path('author/<int:pk>/interest/', RecordInterest.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 Book utilisé par ListView
Comme nous avons accès à l’éditeur Publisher des livres que nous souhaitons afficher, nous surchargeons simplement get_queryset() et utilisons le gestionnaire inverse de clé étrangère de l’objet Publisher.
Le jeu de requête Publisher utilisé par get_object()
Nous comptons sur l’implémentation par défaut de get_object() pour récupérer le bon objet Publisher. Cependant, nous devons explicitement transmettre un paramètre queryset car sinon, l’implémentation par défaut de get_object() appellerait get_queryset() que nous avons surchargée pour renvoyer des objets Book au lieu d’objets Publisher.

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 PublisherDetail:

from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher

class PublisherDetail(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 :

Indice

Chacune de vos vues ne devrait utiliser que des mixins ou des vues en provenance de l’un des groupes de vues génériques fondées sur les classes : détail, liste, édition et date. Par exemple, il est convenable de combiner TemplateView (vue intégrée) avec MultipleObjectMixin (liste générique), mais vous rencontrerez sûrement des problèmes si vous combinez SingleObjectMixin (détail générique) avec MultipleObjectMixin (liste générique).

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 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 AuthorDetail:

# 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 AuthorDetail(FormMixin, DetailView):
    model = Author
    form_class = AuthorInterestForm

    def get_success_url(self):
        return reverse('author-detail', kwargs={'pk': self.object.pk})

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = self.get_form()
        return context

    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() ne sert qu’à 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, et surcharger get_context_data() pour que le formulaire Form soit disponible dans les données de contexte.

Une meilleure solution

Il est assez évident que 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, il serait relativement facile d’écrire simplement la méthode post() vous-même, 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 simple 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 AuthorDisplay 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.views.generic import DetailView
from django import forms
from books.models import Author

class AuthorInterestForm(forms.Form):
    message = forms.CharField()

class AuthorDisplay(DetailView):
    model = Author

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = AuthorInterestForm()
        return context

Puis pour AuthorInterest, il s’agit d’une simple 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 AuthorDisplay utilise lors de la requête GET:

from django.urls import reverse
from django.http import HttpResponseForbidden
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin

class AuthorInterest(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})

Pour terminer, nous lions le tout en créant une nouvelle vue AuthorDetail. Nous savons déjà que l’appel à as_view() sur une vue fondée sur les classes nous renvoie un objet se comportant exactement comme une vue de type fonction. Nous pouvons donc effectuer cet appel au moment où il faut choisir entre les deux « sous-vues ».

Vous pouvez bien sûr transmettre des paramètres nommés à as_view() comme vous le feriez dans la configuration d’URL, par exemple si vous vouliez que le comportement de AuthorInterest puisse aussi être utilisé avec une autre URL mais en affichant un gabarit différent :

from django.views import View

class AuthorDetail(View):

    def get(self, request, *args, **kwargs):
        view = AuthorDisplay.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = AuthorInterest.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 simple 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.

Ce mixin fournit une méthode render_to_json_response() ayant la même signature que render_to_response(). Pour l’utiliser, nous devons simplement la combiner par exemple à une classe TemplateView et surcharger render_to_response() afin d’appeler render_to_json_response() à la place :

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 django.views.generic.detail.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.

Si vous avez de réels goûts d’aventure, vous pourriez même tenter de combiner avec une sous-classe de DetailView capable de renvoyer à la fois du contenu HTML et JSON en fonction d’une propriété de la requête HTTP, telle qu’un paramètre de requête ou un en-tête HTTP. Il suffit de combiner JSONResponseMixin avec SingleObjectTemplateResponseMixin et de surcharger l’implémentation de render_to_response() pour déléguer le rendu à la méthode appropriée en fonction du type de réponse demandée par l’utilisateur :

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.

Back to Top