<< précédentsuivant >>

Chapitre 9 : Vues génériques

Voici à nouveau un thème récurrent de ce livre : au pire, le développement web est ennuyeux et monotone. Jusqu’ici, nous n’avons abordé que la manière dont Django tente de s’éloigner un peu de cette monotonie avec les couches de modèle de gabarit, mais les développeurs web rencontrent aussi cet ennui au niveau des vues.

Les vues génériques de Django ont été développées pour diminuer cette douleur. Elles prennent certains idiomes et patron courants trouvés dans le développement des vues et en font abstraction de façon à ce que vous puissiez rapidement écrire des vues courantes de données sans avoir à écrire trop de code. En fait, presque tous les exemples de vue dans les chapitres précédents peuvent être réécrits à l’aide des vues génériques.

Le chapitre 8 abordait brièvement une méthode permettant de rendre une vue « générique ». En guise de révision, nous pouvons extraire certaines tâches courantes, tel que l’affichage d’une liste d’objets, et écrire le code qui affiche une liste pour n’importe quel objet. Ensuite, le modèle en question peut être transmis comme un argument optionnel à l’URLconf.

Django est livré avec les vues génériques permettant de faire ce qui suit :

  • Réaliser des tâches courantes « simples » : redirection vers une page différente et rendu d’un gabarit donné.
  • Afficher une liste et les détails des pages pour un objet unique. Les vues des listes event_list et entry_list du chapitre 8 sont des exemples de vues en liste. Une page d’événement unique est un exemple de ce que nous appelons une vue « détail ».
  • Présenter les objets basés sur les dates dans des pages d’archives année/mois/jour, les détails associés, et les pages « récentes ». Les archives année, mois, jour du blog de Django (http://www.djangoproject.com/weblog/) sont construites grâce à celles-ci, ce qui est typique des archives du journal.
  • Permettre aux utilisateurs de créer, mettre à jour et supprimer des objets - avec ou sans autorisation.

Associées, ces vues fournissent des interfaces simples pour faire les tâches les plus courantes rencontrées par les développeurs.

Utilisation des vues génériques

Toutes ces vues sont utilisés en créant des dictionnaires dans vos fichiers de configuration URLconf et en passant ces dictionnaires comme troisième membre du tuple de l’URLconf, pour un schéma donné.

Par exemple, voici un simple URLconf que vous pouvez utiliser pour présenter une page statique « à propos » :

from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template

urlpatterns = patterns('',
    ('^about/$', direct_to_template, {
        'template': 'about.html'
    })
)

Même si cela peut paraître un peu « magique » à première vue — regardez, une vue sans aucun code ! — c’est en fait exactement la même chose que les exemples du chapitre 8 : la vue direct_to_template récupère simplement les informations depuis les paramètres supplémentaires du dictionnaire et utilise cette information lors du rendu de la vue.

Puisque cette vue générique — et toutes les autres — est une fonction de vue comme les autres, nous pouvons la réutiliser à l’intérieur de nos propres vues. Par exemple, étendons notre « à propos » pour faire correspondre les URLs du formulaire /about/<whatever>/ et rendre statiquement about/<whatever>.html. Nous faisons cela en modifiant l’URLconf pour qu’il pointe vers une fonction de la vue :

from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template
**from mysite.books.views import about_pages**

urlpatterns = patterns('',
    ('^about/$', direct_to_template, {
        'template': 'about.html'
    }),
    **('^about/(w+)/$', about_pages),**
)

Ensuite, nous écrivons la vue about_pages :

from django.http import Http404
from django.template import TemplateDoesNotExist
from django.views.generic.simple import direct_to_template

def about_pages(request, page):
    try:
        return direct_to_template(request,
        template="about/%s.html" % page)
    except TemplateDoesNotExist:
        raise Http404()

Ici nous traitons direct_to_template comme n’importe quelle autre fonction. La seule chose un peu délicate ici consiste à gérer les gabarits manquants. Nous ne voulons pas d’un gabarit inexistant qui provoquerait une erreur du serveur, nous récupérons donc les exceptions TemplateDoesNotExist pour les remplacer par des erreurs 404.

Y a-t-il ici une faille de sécurité ?

Les lecteurs attentifs ont peut être noté l’existance d’une faille de sécurité potentielle : nous contruisons le nom de gabarit en utilisant du contenu interprété depuis le navigateur (template="about/%s.html" % page). De prime abord, ceci ressemble à une faille classique de type directory traversal (ndt : traversée de répertoire, abordée en détail au chapitre 19). Mais est-ce réellement cela ?

Pas exactement. Oui, une valeur page malveilleusement créée peut causer une traversée de répertoire, mais puisque page est extraite de l’URL de requête, toutes les valeurs ne seront pas acceptées. La clef est dans l’URLconf : nous utilisons l’expression rationnelle \w+ pour faire correspondre la partie page de l’URL, et \w n’accepte que les lettres et les chiffres. Ainsi, tout charactère malveillant (ici, points et slashs) seront rejetés par l’analyseur d’URL avant d’arriver à la vue elle même.

Vues génériques des objets

direct_to_template est certainement très utile, mais les vues génériques de Django brillent réellement lorsqu’il faut appliquer les vues sur votre contenu de la base de données. Puisqu’il s’agit d’une tâche courante, Django est fourni avec une poignée de vues génériques préconçues qui rends la génération de listes et de vues détaillées des objets incroyablement aisée.

Jetons un œil à l’une de ces vues génériques : la vue « liste d’objet ». Nous utiliserons l’objet Publisher du chapitre 5 :

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

    def __str__(self):
        return self.name

    class Meta:
        ordering = ["-name"]

    class Admin:
        pass

Pour faire une page listant tous les livres, nous utilisons un URLconf au fil des ces lignes :

from django.conf.urls.defaults import *
from django.views.generic import list_detail
from mysite.books.models import Publisher

publisher_info = {
    "queryset" : Publisher.objects.all(),
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info)
)

C’est le seul code Python que nous avons besoin d’écrire. Nous avons cependant toujours besoin d’écrire un gabarit. Nous pouvons explicitement indiquer à la vue object_list quel est le gabarit à utiliser en ajoutant une clef template_name dans les arguments supplémentaires du dictionnaire. En l’absence d’un gabarit explicite, Django va en déduire un à partir du nom de l’objet.

Dans ce cas, le gabarit déduit sera "books/publisher_list.html" — la partie « books » provient du nom de l’application qui définit le modèle, alors que la partie « publisher » est simplement la version « bas de casse » du nom du modèle.

Ce gabarit sera généré d’après un contexte contenant une variable appelée object_list qui se compose de tous les objets livre. Un gabarit très simple pourrait ressembler à celui qui suit :

{% extends "base.html" %}

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

C’est vraiment tout ce qu’il y a à faire. Toutes les sympathiques fonctionnalités des vue génériques proviennent du changement de dictionnaire « info » transmis à la vue générique. L’annexe D documente toutes les vues génériques et toutes leurs options en détail ; le reste de ce chapitre abordera quelques unes des manières courantes pour personnaliser et pour étendre les vues génériques.

Étendre les vues génériques

L’utilisation de vues génériques peut sans aucuns doutes considérablement accélérer le développement. Dans la plupart des projets, cependant, il arrive un moment où les vues génériques ne suffisent plus. En effet, la question la plus fréquente des nouveaux développeurs Django est «comment faire pour que les vues génériques prennent en charge un champs plus large de situations ?».

Heureusement, dans la plupart de ces cas, il existe des méthodes simples pour étendre les vues génériques afin qu’elles gèrent un plus large choix de cas d’utilisation. Ces situations peuvent généralement être réparties en une poignée de modèles traités dans les sections qui suivent.

Faire des contextes de gabarit « sympathiques »

Vous avez peut être remarqué que l’exemple de gabarit de la liste « éditeur » stocke tous les livres dans une variable nommée object_list. Bien que cela fonctionne bien, tout n’est pas si « rose » pour les auteurs de gabarits : ils doivent « savoir » qu’ils ont ici affaire à des livres. Un meilleur nom pour cette variable pourrait être publisher_list ; le contenu de cette variable est évident.

Nous pouvons changer le nom de cette variable facilement avec l’argument template_object_name :

publisher_info = {
    "queryset" : Publisher.objects.all(),
    **"template_object_name" : "publisher",**
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info)
)

Fournir un template_object_name est toujours une bonne idée. Vos collègues qui écrivent des gabarits vous remercierons.

Ajouter un context supplémentaire

Souvent vous avez simplement besoin de présenter quelques informations supplémentaires à celles fournies par la vue générique. Par exemple, montrer une liste de tous les autres éditeurs sur chaque page détaillant un éditeur. La vue générique object_detail fournie l’éditeur au contexte, mais il semble qu’il ne soit pas possible d’obtenir la liste de tous les éditeurs dans ce gabarit.

C’est pourtant le cas : toutes les vues génériques acceptent un argument optionnel, extra_context. C’est un dictionnaire d’objets qui pourra être ajouté au contexte du gabarit. Ainsi, pour fournir la liste de tous les éditeurs sur la vue détail, nous utiliserons un dictionnaire info comme ceci :

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    **"extra_context" : {"book_list" : Book.objects.all()}**
}

Ceci remplira une variable {{ book_list }} dans le contexte du gabarit. Ce patron peut être utilisé pour transmettre toute information vers le gabarit pour la vue générique. C’est très commode.

Cependant, il y a un bug subtil ici — pouvez vous le trouver ?

Le problème se présente lorsque les requêtes dans extra_context sont évaluées. Puisque cet exemple place Publisher.objects.all() dans l’URLconf, il sera évalué une seule fois (lorsque l’URLconf est chargé la première fois). Une fois que vous ajoutez ou retirez des éditeurs, vous noterez que la vue générique ne reflète pas ces changements jusqu’à ce vous rechargiez le serveur web (voir « Cache et QuerySet » dans l’annexe C pour plus d’information sur le moment où les Querysets sont mis en cache et évalués).

Note

Ce problème ne s’applique pas à l’arguement queryset de la vue générique. Puisque Django sait que ce QuerySet particulier ne doit jamais être mis en cache, la vue générique prend soin d’effacer le cache lors du rendu de chacune des vues.

La solution consiste à utiliser un « callback » dans extra_context au lieu d’une valeur. N’importe quel « callable » (autrement dit une fonction « appelable ») passé à extra_context sera évalué lorsque la vue est rendue (en lieu et place d’une unique fois). Vous pouvez faire cela avec une fonction explicitement définie :

def get_books():
    return Book.objects.all()

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : **{"book_list" : get_books}**
}

vous pouvez aussi utiliser une version moins évidente mais plus courte, reposant sur le fait que Publisher.objects.all soit elle même appellable :

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : **{"book_list" : Book.objects.all}**
}

Notez l’absence de parenthèses après Book.objects.all ; ceci référence la fonction sans réellement l’appeler (ce que fera la vue générique par la suite).

Voir des sous-ensembles d’objets

Maintenant, intéressons nous à cette clef queryset que nous avons utilisé jusqu’à présent. La plupart des vues génériques prennent un des ces arguments queryset — C’est ainsi que la vue sait quel jeu d’objet il faut afficher (consultez « Selectionner des objets » au chapitre 5 pour une introduction aux QuerySets, et l’annexe C pour les détails complets).

Pour prendre un exemple simple, nous voudrions ordonner une liste de livres par date de publication, le plus récent en premier :

book_info = {
    "queryset" :
    Book.objects.all().order_by("-publication_date"),
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    **(r'^books/$', list_detail.object_list, book_info),**
)

C’est un exemple relativement simple, mais il illustre bien l’idée. Bien sûr, vous voudrez généralement faire plus que simplement réordonner des objets. Si vous voulez présenter une liste des livres selon un éditeur particulier, vous pouvez utiliser la même technique :

**apress_books = {**
    **"queryset": Book.objects.filter(publisher__name="Apress
    Publishing"),**
    **"template_name" : "books/apress_list.html"**
**}**

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    **(r'^books/apress/$', list_detail.object_list,
    apress_books),**
)

Remarquez que même avec un queryset filtré, nous utilisons aussi un nom de gabarit personnalisé. Dans le cas contraire, la vue générique utiliserait le même gabarit que la liste native des objets, ce qui pourrait ne pas être ce que nous voulons. Notez aussi que ce n’est pas une façon très élégante de faire des livres spécifiques à des éditeurs. Si nous voulons ajouter une nouvelle page éditeurs, nous aurons besoin d’une autre poignée de lignes dans l’URLconf, et cela ne serait pas raisonnable au delà de quelques éditeurs. Nous aborderons ce problème à la section suivante.

Note

Si vous obtenez un 404 lors d’une requête /books/apress/, vérifiez que vous avez réellement un éditeur avec le nom « Apress Publishing ». Les vues génériques possèdent un paramètre allow_empty pour ce cas précis. Lisez l’annexe D pour plus de détails.

Filtrages complexes à l’aide de l’encapsulation de fonction

Un autre besoin courant est de filtrer les objets donnés dans une page liste, selon les clefs dans l’URL. Auparavant, nous avons codé le nom de l’éditeur dans l’URLconf, mais si nous avions voulu écrire une vue qui affiche tous les livres selon un éditeur arbitraire ? Nous pouvons envelopper la vue générique object_list pour éviter d’écrire beaucoup de code à la main. Comme d’habitude, nous commençerons par écrire un URLconf :

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    **(r'^books/(w+)/$', books_by_publisher),**
)

Ensuite, nous écrirons la vue books_by_publisher elle même :

from django.http import Http404
from django.views.generic import list_detail
from mysite.books.models import Book, Publisher

def books_by_publisher(request, name):

    # Look up the publisher (and raise a 404 if it can't be
    found).
    try:
        publisher = Publisher.objects.get(name__iexact=name)
    except Publisher.DoesNotExist:
        raise Http404

    # Use the object_list view for the heavy lifting.
    return list_detail.object_list(
        request,
        queryset = Book.objects.filter(publisher=publisher),
        template_name = "books/books_by_publisher.html",
        template_object_name = "books",
        extra_context = {"publisher" : publisher}
    )

Ceci fonctionne parcequ’il n’y a vraiment rien de spécial au sujet des vues génériques — ce sont juste des fonctions Python ? Comme toute fonction de vue, les vues génériques attendent un certain jeu d’arguments et retournent des objets HttpResponse. Ainsi, il est incroyablement facile d’encapsuler une petite fonction autour d’une vue générique qui fait le travail supplémentaire avant (ou après ; voyez la prochaine section) et remet le résultat à la vue générique.

Note

Notez que dans l’exemple précédent nous passons l’éditeur courant à l’affichage dans un extra_context. C’est généralement une bonne idée dans des enveloppes de cette nature ; cela permet au gabarit d’être informé de l’objet « parent » actuellement parcouru.

Faire des actions supplémentaires

Le dernier patron commun que nous verrons implique quelque travail supplémentaire avant ou après l’appel de la vue générique.

Imaginez que nous ayons un champ last_accessed pour l’objet Author que nous utilisions dans le but de garder la trace de la dernière fois où quelqu’un à consulté cet auteur. La vue générique object_detail, bien sur, ne saura rien de ce champs. Encore une fois, nous pouvons facilement écrire une vue personnalisée pour garder ce champs à jour.

Tout d’abord, nous devons ajouter quelques détails dans l’URLconf afin de pointer sur une vue personnalisée :

from mysite.books.views import author_detail

urlpatterns = patterns('',
    #...
    **(r'^authors/(?P<author_id>d+)/$', author_detail),**
)

Ensuite nous écrivons notre fonction enveloppe :

import datetime
from mysite.books.models import Author
from django.views.generic import list_detail
from django.shortcuts import get_object_or_404

def author_detail(request, author_id):
    # Look up the Author (and raise a 404 if she's not found)
    author = get_object_or_404(Author, pk=author_id)

    # Record the last accessed date
    author.last_accessed = datetime.datetime.now()
    author.save()

    # Show the detail page
    return list_detail.object_detail(
        request,
        queryset = Author.objects.all(),
        object_id = author_id,
    )

Note

Ce code ne fonctionnera pas vraiment à moins que vous n’ajoutiez un champ last_accessed à votre modèle Author et créiez un gabarit books/author_detail.html.

Nous pouvons utiliser une syntaxe similaire pour altérer la réponse retournée par la vue générique. Si nous voulons fournir une version texte téléchargeable de la liste des auteurs, nous pouvons utiliser une vue telle que :

def author_list_plaintext(request):
   response = list_detail.object_list(
       request,
       queryset = Author.objects.all(),
       mimetype = "text/plain",
       template_name = "books/author_list.txt"
   )
   response["Content-Disposition"] = "attachment;
   filename=authors.txt"
   return response

Ceci fonctionne parce que les vues génériques renvoient de simple objets HttpResponse qui peuvent être considérés comme des dictionnaires pour paramétrer les entêtes HTTP. Cette « disposition de contenu » , de ce fait, indique au navigateur de télécharger et de sauvegarder la page au lieu de l’afficher.

Et ensuite ?

Dans ce chapitre nous avons vu seulement quelques unes des vues génériques fournies par Django, mais les concepts généraux présentés ici s’appliquent de façon similaire à n’importe quelle vue générique. L’annexe D couvre toutes les vues disponibles en détail, et il est recommandé de la lire si vous voulez bénéficier de toute la puissance de cette fonctionnalité.

Au chapitre suivant, nous plongerons plus profondemment dans le fonctionnement interne des gabarits sous Django, montrant toutes les sympathiques façons de les étendre. Jusqu’à présent, nous avons traité le moteur de gabarit comme un outil essentiellement statique que vous pouvez utiliser pour le rendu de votre contenu.

<< précédentsuivant >>

Dernière modification: 2008-09-04 14:49:31.428995