Vues génériques fondées sur les classes, fournies par Django

L’écriture d’applications Web peut être une tâche monotone, car certains motifs se répètent encore et encore. Django essaie d’enlever une partie de cette monotonie au niveau des modèles et des gabarits, mais les développeurs Web font également face à ces répétitions au niveau des vues.

Les vues génériques de Django ont été développées pour résoudre ce problème. Elles s’attachent à trouver des motifs ou idiomes courants dans le développement des vues et à les abstraire de façon à ce qu’on puisse rapidement écrire des vues courantes sans devoir écrire trop de code.

Il est possible d’identifier certaines tâches courantes, comme l’affichage d’une liste d’objets, et d’écrire du code qui affiche une liste de n’importe quel objet. Puis, le modèle concerné peut être transmis comme paramètre supplémentaire dans la configuration d’URL.

Django est livré avec des vues génériques pour accomplir les choses suivantes :

  • Afficher des pages de liste et de détail pour un seul type d’objet. Si nous devions créer une application pour gérer des conférences, une vue TalkListView et une vue RegisteredUserListView seraient des exemples de vues de listes. Une page d’une seule conférence pourrait être un exemple de ce que nous appelons une vue de « détail ».
  • Présenter des objets datés dans des pages d’archive par année/mois/jour, avec les détails associés et des pages « éléments récents ».
  • Permettre à des utilisateurs de créer, mettre à jour et supprimer des objets, avec ou sans gestion des autorisations.

Considérées globalement, ces vues offrent des interfaces pour effectuer les tâches les plus courantes que rencontrent les développeurs.

Extension des vues génériques

Il est indiscutable que l’utilisation des vues génériques peut considérablement accélérer le développement. Dans la plupart des projets, cependant, on atteint souvent un stade où les vue génériques ne suffisent plus. En fait, la question la plus fréquemment posée par les nouveaux développeurs Django est de savoir comment il est possible de gérer plus de variétés de situations avec les vues génériques.

C’est l’une des raisons pour lesquelles les vues génériques ont été revues pour la version 1.3 ; précédemment, il s’agissait de fonctions de vue avec une très large diversité d’options. Maintenant, au lieu de transmettre une grosse partie de configuration dans la configuration d’URL, la manière recommandée d’étendre les vues génériques est d’en faire des sous-classes et de surcharger leurs attributs et méthodes.

Ceci dit, les vues génériques ont leurs limites. Si vous vous retrouvez à vous battre pour implémenter vos vues comme des sous-classes de vues génériques, il se pourrait qu’il soit plus efficace d’écrire simplement le code dont vous avez besoin dans vos vues, que ce soit par des fonctions ou des classes.

Vous trouverez davantage d’exemples de vues génériques dans certaines applications tierces, mais n’hésitez pas à écrire vos propres vues selon vos besoins.

Vues génériques d’objets

TemplateView est certainement utile, mais les vues génériques de Django exposent tout leur potentiel lorsqu’il s’agit de présenter des vues de contenu de la base de données. Dans la mesure où c’est une tâche très courante, Django contient un ensemble de vues génériques intégrées pour aider à générer des vues de liste et de détail pour les objets.

Commençons par examiner quelques exemples d’affichage d’une liste d’objets ou d’un objet individuel.

Nous allons utiliser ces modèles :

# models.py
from django.db import models


class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    class Meta:
        ordering = ["-name"]

    def __str__(self):
        return self.name


class Author(models.Model):
    salutation = models.CharField(max_length=10)
    name = models.CharField(max_length=200)
    email = models.EmailField()
    headshot = models.ImageField(upload_to="author_headshots")

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField("Author")
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    publication_date = models.DateField()

Nous devons maintenant définir une vue :

# views.py
from django.views.generic import ListView
from books.models import Publisher


class PublisherListView(ListView):
    model = Publisher

Et finalement connecter cette vue à une URL :

# urls.py
from django.urls import path
from books.views import PublisherListView

urlpatterns = [
    path("publishers/", PublisherListView.as_view()),
]

Voilà tout le code Python qu’il suffit d’écrire. Il faut cependant encore écrire un gabarit. Nous pourrions indiquer explicitement à la vue le gabarit à utiliser en lui ajoutant un attribut template_name, mais en l’absence d’un gabarit explicite, Django déduit un nom de gabarit à partir du nom de l’objet. Dans ce cas, le gabarit « automatique » sera "books/publisher_list.html", la partie « books » provient du nom de l’application contenant ce modèle alors que « publisher » provient du nom du modèle en minuscules.

Note

Ainsi, lorsque (par exemple) l’option APP_DIRS d’un moteur DjangoTemplates est définie à True dans TEMPLATES, un emplacement de gabarit pourrait être : /chemin/vers/projet/books/templates/books/publisher_list.html.

Ce gabarit sera rendu avec un contexte contenant une variable appelée object_list contenant elle-même les objets Publisher. Un gabarit pourrait ressembler à ceci :

{% extends "base.html" %}

{% block content %}
    <h2>Publishers</h2>
    <ul>
        {% for publisher in object_list %}
            <li>{{ publisher.name }}</li>
        {% endfor %}
    </ul>
{% endblock %}

Et voilà, tout est là. Toutes les fonctionnalités sympathiques des vues génériques proviennent des modifications d’attributs définis dans la vue générique. La référence des vues génériques documente en détails toutes les vues génériques et leurs options. La suite de de document s’attelle à décrire certaines manières fréquentes de personnaliser et étendre les vues génériques.

Construction de contextes de gabarits « astucieux »

Vous avez peut-être constaté que notre exemple de gabarit de liste d’éditeurs stocke tous les éditeurs dans une variable nommée object_list. Même si cela fonctionne très bien, ce n’est pas très « sympa » pour les auteurs de gabarits : ils doivent simplement savoir qu’ils manipulent ici des éditeurs.

Si vous avez affaire à un objet de modèle, c’est déjà fait pour vous. Lorsque vous avez affaire à un objet ou un jeu de requête, Django est capable de renseigner le contexte en utilisant la version en minuscules du nom de la classe de modèle. Ceci s’ajoute à l’élément object_list par défaut, mais contient exactement les mêmes données, c’est-à-dire publisher_list.

Si ce n’est toujours pas un bon nom, vous pouvez indiquer explicitement le nom de la variable de contexte. L’attribut context_object_name d’une vue générique indique le nom de variable de contexte à utiliser :

# views.py
from django.views.generic import ListView
from books.models import Publisher


class PublisherListView(ListView):
    model = Publisher
    context_object_name = "my_favorite_publishers"

La mise à disposition d’un nom context_object_name signifiant est toujours une bonne idée. Vos collaborateurs rédacteurs de gabarits vous remercieront.

Ajout de contexte supplémentaire

Il est souvent nécessaire de compléter par quelques informations supplémentaires au-delà de ce que la vue générique fournit. Par exemple, on peut penser à afficher une liste de livres sur la page de détail des éditeurs. La vue générique DetailView place l’éditeur dans le contexte, mais comment ajouter d’autres informations dans le gabarit ?

La réponse consiste à créer une sous-classe de DetailView et d’y placer votre propre implémentation de la méthode get_context_data. L’implémentation par défaut ajoute l’objet à afficher dans le gabarit, mais en la surchargeant, vous pouvez l’enrichir :

from django.views.generic import DetailView
from books.models import Book, Publisher


class PublisherDetailView(DetailView):
    model = Publisher

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context
        context = super().get_context_data(**kwargs)
        # Add in a QuerySet of all the books
        context["book_list"] = Book.objects.all()
        return context

Note

Généralement, get_context_data fusionne les données de contexte de toutes les classes parentes avec celles de la classe actuelle. Pour préserver ce comportement dans vos propres classes où vous souhaitez modifier le contexte, il faut prendre soin d’appeler get_context_data de la classe parente (super). Pour autant qu’il n’y ait pas deux classes essayant de modifier la même clé, le résultat sera correct. Cependant, si l’une des classes tente de surcharger une clé après qu’une classe parente l’a définie (après l’appel à super), toute classe enfant de cette classe devra aussi définir explicitement cette clé après super pour être certain de surcharger tous les parents. Si cela vous cause des ennuis, analysez l’ordre de résolution des méthodes de votre vue.

Un autre élément à signaler est que les données de contexte provenant des vues génériques basées sur les classes vont écraser les données fournies par les processeurs de contexte ; voir get_context_data() comme exemple.

Affichage de sous-ensembles d’objets

Examinons maintenant un peu plus attentivement le paramètre model que nous avons toujours utilisé. Ce paramètre, qui définit le modèle de base de données sur lequel la vue va porter, est disponible pour toutes les vues génériques qui opèrent sur un seul objet ou une liste d’objets. Cependant, le paramètre model n’est pas le seul moyen de définir le type d’objet d’une vue, il est aussi possible de définir la liste des objets par le paramètre queryset:

from django.views.generic import DetailView
from books.models import Publisher


class PublisherDetailView(DetailView):
    context_object_name = "publisher"
    queryset = Publisher.objects.all()

L’indication de model = Publisher est un raccourci pour dire queryset = Publisher.objects.all(). Cependant, en utilisant queryset pour définir une liste d’objets filtrée, vous pouvez spécifier plus finement les objets qui seront visibles dans la vue (voir Création de requêtes pour plus d’informations à propos des objets QuerySet, et consultez la référence des vues fondées sur les classes pour obtenir tous les détails).

Comme exemple, essayons de trier une liste de livres par date de publication, les plus récents en premier :

from django.views.generic import ListView
from books.models import Book


class BookListView(ListView):
    queryset = Book.objects.order_by("-publication_date")
    context_object_name = "book_list"

Il s’agit d’un exemple très minimal, mais qui montre bien le principe de fonctionnement. Le but est généralement de faire plus que de simplement trier les objets. Si vous souhaitez présenter une liste de livres provenant d’un éditeur particulier, la même technique peut être utilisée :

from django.views.generic import ListView
from books.models import Book


class AcmeBookListView(ListView):
    context_object_name = "book_list"
    queryset = Book.objects.filter(publisher__name="ACME Publishing")
    template_name = "books/acme_list.html"

Remarquez qu’en plus d’un paramètre queryset filtré, nous indiquons également un nom de gabarit personnalisé. Sans cela, la vue générique utiliserait le même gabarit que la liste d’objets « standard », ce qui ne correspondrait pas au but recherché.

Remarquez également que ce n’est pas une façon très élégante d’afficher les livres d’un éditeur particulier. Si nous voulions ensuite ajouter une page pour un autre éditeur, nous devrions ajouter des lignes supplémentaires dans la configuration d’URL et nous serions pratiquement limités à quelques éditeurs. Nous aborderons ce problème dans la section suivante.

Note

Si vous obtenez une erreur 404 en accédant à /books/acme/, vérifiez que vous possédez bien un objet Publisher dont le nom est « ACME Publishing ». Les vues génériques ont un paramètre allow_empty pour cette situation. Consultez la référence des vues fondées sur les classes pour plus de détails.

Filtrage dynamique

Un autre besoin fréquent est de filtrer les objets d’une liste selon un critère de l’URL. Plus haut, nous avons figé le nom de l’éditeur dans la configuration d’URL, mais comment faire si nous voulions écrire une vue affichant tous les livres d’un éditeur librement choisi ?

Heureusement, la classe ListView possède une méthode get_queryset() que nous pouvons surcharger. Par défaut, elle renvoie la valeur du paramètre queryset, mais on peut l’utiliser pour ajouter de la logique supplémentaire.

L’élément clé pour que cela fonctionne est qu’au moment où les vues fondées sur les classes sont appelées, divers attributs utiles sont créés pour self. En plus de la requête elle-même (self.request), il y a aussi les paramètres positionnels (self.args) et nommés (self.kwargs) capturés en fonction de la configuration d’URL.

Ici, nous avons une configuration d’URL comportant un seul groupe capturé :

# urls.py
from django.urls import path
from books.views import PublisherBookListView

urlpatterns = [
    path("books/<publisher>/", PublisherBookListView.as_view()),
]

Puis, nous allons écrire la vue PublisherBookListView:

# views.py
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from books.models import Book, Publisher


class PublisherBookListView(ListView):
    template_name = "books/books_by_publisher.html"

    def get_queryset(self):
        self.publisher = get_object_or_404(Publisher, name=self.kwargs["publisher"])
        return Book.objects.filter(publisher=self.publisher)

Utiliser get_queryset pour ajouter de la logique à la sélection du jeu de requête est aussi pratique que puissant. Nous pourrions par exemple filtrer en se basant sur l’utilisateur connecté (self.request.user) ou toute autre logique plus complexe.

Nous pouvons aussi ajouter en même temps l’éditeur dans le contexte, afin de pouvoir l’utiliser dans le gabarit :

# ...


def get_context_data(self, **kwargs):
    # Call the base implementation first to get a context
    context = super().get_context_data(**kwargs)
    # Add in the publisher
    context["publisher"] = self.publisher
    return context

Autres opérations supplémentaires

Le dernier usage que nous examinerons concerne les opérations supplémentaires effectuées avant ou après l’appel à la vue générique.

Imaginez que nous ayons un champ last_accessed dans le modèle Author qui nous sert à garder la trace du moment le plus récent où quelqu’un a consulté la page de cet auteur :

# models.py
from django.db import models


class Author(models.Model):
    salutation = models.CharField(max_length=10)
    name = models.CharField(max_length=200)
    email = models.EmailField()
    headshot = models.ImageField(upload_to="author_headshots")
    last_accessed = models.DateTimeField()

La classe générique DetailView n’a aucune conscience de la présence de ce champ, mais nous pouvons une nouvelle fois écrire une vue personnalisée pour maintenir ce champ à jour.

Tout d’abord, nous devons ajouter une ligne de détail d’auteur dans la configuration d’URL pour faire le lien avec une vue personnalisée :

from django.urls import path
from books.views import AuthorDetailView

urlpatterns = [
    # ...
    path("authors/<int:pk>/", AuthorDetailView.as_view(), name="author-detail"),
]

Puis il s’agit d’écrire la nouvelle vue. get_object étant la méthode qui récupère l’objet, nous la surchargeons en « enveloppant » l’appel par défaut :

from django.utils import timezone
from django.views.generic import DetailView
from books.models import Author


class AuthorDetailView(DetailView):
    queryset = Author.objects.all()

    def get_object(self):
        obj = super().get_object()
        # Record the last accessed date
        obj.last_accessed = timezone.now()
        obj.save()
        return obj

Note

Cette configuration d’URL emploie le group nommé pk, il s’agit là du nom par défaut utilisé par DetailView pour trouver la valeur de clé primaire à utiliser pour filtrer le jeu de requête.

Si vous aimeriez nommer différemment le groupe, vous pouvez définir pk_url_kwarg dans la vue.

Back to Top