Introduction

Si vous utilisez Django vous connaissez probablement déjà les vues génériques/generic views : elles permettent de faciliter la création de certaines tâches monotones et répétitives qu'on retrouve souvent sur les applications web comme sont les listes, affichage, modification et création d'objets...

Jusqu'à Django 1.2 les vues génériques étaient des fonctions, et depuis Django 1.3 (qui est sorti il y a peu) elles sont maintenant basées sur des classes. Je vais tenter ici, modestement, de montrer l'intérêt de ce changement, donner des exemples simples pour illustrer la migration d'un type vers l'autre, et donner quelques repères pour comprendre leur fonctionnement.

Les vues génériques basées sur des fonctions (qu'on notera FBV) fonctionnent très bien pour des cas simples, puis on doit rapidement faire de l'encapsulation ('wrapper') pour certains cas spéciaux, mais on arrive encore à notre but, mais à un moment ou à un autre, malheureusement on se retrouve coincé sans pouvoir faire ce qu'on veut et on doit alors recréer nos propres vues et se passer de ce qui existe déjà dans Django.

Cette limite est connue depuis longtemps et le travail a été long (le ticket #6735 a trois ans) mais l'arrivée des CBV permet justement de s'affranchir de ses limites.

Exemple de passage des FBV aux CBV

Prenons un cas simple où l'on veut lister des objets, et où la liste de ceux-ci dépendra d'un paramètre dynamique (ici l'utilisateur connecté) :

note : j'ai volontairement allégé le code en enlevant les parties classiques ou évidentes, néanmoins si vous voyez une énormité merci de le signaler

## urls.py
from myproject.views import listview

urlpatterns = patterns('',
    (r'^list/', listview, {}, 'myobject_list'),
)

## models.py
from django.contrib.auth.models import User

class MyObject(models.Model):
    ...
    author = models.ForeignKey(User)

## views.py
@login_required
def listview(request,
          queryset=MyObject.objects.all(),
          template_name='myproject/myobject_list.html'):
    qs = queryset.filter(author=request.user)
    return object_list(request,
                       queryset=qs,
                      template_name=template_name)

On a ici l'exemple typique de l'utilisation d'une vue générique avec un wrapper. Jusque là ça ne pose pas de problème particulier et tout peut être fait avec Django 1.2 et ses FBV.

Par contre, vu que dans notre modèle author est un utilisateur, on imagine très bien qu'il faut qu'à l'enregistrement ce champ soit initialisé avec l'utilisateur connecté, donc la valeur de request.user, et là les vues génériques montrent leur limitation : il est en effet possible de surcharger la fonction save() du formulaire mais il n'est pas possible d'indiquer à la vue générique create_object de lui passer le paramètre qui nous sera utile au moment de son enregistrement.

On est donc obligé de créer notre propre vue pour gérer ce qui est finalement une différence mineure mais un cas que l'on retrouve fréquemment.

On aura alors quelque chose comme

## urls.py
urlpatterns = patterns('',
    (r'^list/', listview, {}, 'myobject_list'),
    (r'^create/', createview, {}, 'myobject_create'),
)

## forms.py
class MyObjectForm(ModelForm):
    class Meta:
        model = MyObject
        exclude = ('author',)

    def save(self, user=None):
        myobject = super(MyObjectForm, self).save(commit=False)
        myobject.author = user
        myobject.save()

## views.py
from myproject.forms import MyObjectForm

@login_required
def createview(request, form_class=MyObjectForm):
    if request.method == 'POST':
        form = form_class(request.POST)
        if form.is_valid():
            myobject = form.save(user=request.user)
            return HttpResponseRedirect(myobject.get_absolute_url())
    else:
        form = form_class()
    return render_to_response('myproject/myobject_form.html',
                              {'form': form},
                              context_instance=RequestContext(request))

Voyons maintenant comment les deux exemples précédents sont implémentables en utilisant les vues basées sur les classes.

## views.py

class ListView(generic.ListView):
    queryset = MyObject.objects.all()
    template_name = "myproject/myobjects_list.html"

    def get_queryset(self):
        return self.queryset.filter(author=self.request.user)
listview = login_required(ListView.as_view())

pour la liste d'objets, ok le code est un peu plus court, mais ça ne change pas grand-chose finalement. Voyons maintenant la vue implémentant la création d'objet avec l'enregistrement de l'utilisateur connecté dans le champ author

## views.py

from myproject.forms import MyObjectForm

class CreateView(generic.CreateView):
    form_class = MyObjectForm
    template_name = "myproject/myobject_form.html"

    def form_valid(self, form):
        self.object = form.save(user=self.request.user)
        return super(CreateView, self).form_valid(form)
createview = login_required(CreateView.as_view())   

Ici on arrive à réutiliser toute le code fourni par Django et ne redéfinir que quelques valeurs (form_class, template_name) et juste la fonction form_valid() c'est-à-dire celle qui définit ce qui se passe quand le formulaire est valide et qu'on veut le sauvegarder.

Utilisation

Comme je l'ai dit en introduction, les class-based views ont été implémentées dans Django 1.3 mais vous avez de la chance puisque que le code a été backporté dans Django 1.2 et vous pouvez donc d'ores et déjà les essayer/utiliser même si vous n'avez pas encore migré en 1.3.

Donc si vous êtes en 1.3 vous devez ajouter l'import suivant dans vos views.py

from django.views import generic

Si vous êtes encore en 1.2, il vous faut installer l'application django-cbv

$ pip install django-cbv

Ajouter le middleware qui va bien dans settings.py

MIDDLEWARE_CLASSES = (
    ...
    'cbv.middleware.DeferredRenderingMiddleware',
)

et, dans vos views.py utiliser l'import suivant :

import cbv as generic

Cette technique, directement tirée de l'article de Bruno Renié (que je remercie puisque c'est cet article et la découverte de cette possibilité qui m'a fait enfin m'intéresser sérieusement aux CBV, tout en restant en Django 1.2), vous permet ensuite de aisément migrer en 1.3 en n'ayant besoin de changer que l'import.

Aller plus loin

Bon, ça c'était un exemple simple mais bien évidemment les CBV sont à la fois plus complexes et plus puissantes. Je vous encourage fortement à aller lire la doc (présentation et référence) ainsi que le code, mais en gros voici comment ça se passe (de ce que j'en ai compris pour l'instant, n'hésitez pas à me corriger si je dis une connerie) :

  • chaque FBV qui existait auparavant existe maintenant sous la forme d'une CBV et vous pouvez retrouver simplement le même comportement
  • chaque vue générique est formée de un ou plusieurs "mixin"
  • chaque mixin implémente une fonctionnalité comme générer un template, afficher une liste d'objets, créer et afficher des formulaires
  • les vues génériques utilisent les capacités d'héritage des classes pour, en mélangeant (mixant ;-) un ou plusieurs mixins, produire le comportement désiré
  • pour redéfinir le comportement d'une vue, il suffit de redéfinir un ou plusieurs paramètres, voire une ou plusieurs fonctions provenant des mixins, comme je l'ai fait dans l'exemple ci-dessus
  • si vous avez besoin d'utiliser plusieurs fois le même comportement, il vous suffit de définir vos propres mixins et de créer vos propres vues en héritant de ceux-ci et de ceux prédéfinis.

Conclusion

Les exemples donnés ici sont simples, et l'article n'a pas pour vocation à vous transformer en 'powerusers' des CBV mais j'espère qu'il vous permettra de vous donner une première vision de leur fonctionnement et vous encouragera à lire la documentation et le code associé pour découvrir leur puissance.

bonus : si quelqu'un sait (ou trouve) comment utiliser les CBV pour gérer une vue avec plusieurs formulaires en même temps je suis preneur de l'information...