Des vues normales aux vues génériques Django 11


Nous avons reçu plusieurs demandes d’explication des class based views et une explication de ce code dans le cadre de notre opération père castor, commente nous un snippet.

Je me suis dis que j’allais faire d’une pierre deux coups et trois mouvements.

Rappel sur les bases des vues

Une vue, c’est une fonction ORDINNAIRE qui prends une requête en paramètre et qui revoie une réponse.

Si vous avez un urls.py qui ressemble à ça:

from mon_app.views import home, tous_les_utilisateurs
from django.contrib.auth.views import login
 
urlpatterns = patterns('',
    url(r'^/', home),
    url(r'^login/', login),
    url(r'^list/', tous_les_utilisateurs),
)

Et une vue dans views.py qui ressemble à ça:

from django.shortcurts import render
from django.contrib.auth.models import User
 
def tous_les_utilisateurs(request):
    context = {"utilisateurs": User.objects.all()}
    return render(request, "tous_les_utilisateur.html", context)

Voilà ce qui va se passer si l’utilisateur visite http://monsite.com/list/:

Fonctionnement d'une vue Django

L'important ici est de comprendre que tout ça fonctionne à base d'arc-en-ciels distillés

Seulement voilà, vous allez vouloir rajouter des trucs, genre, la pagination.

Et votre vue va se changer en ça:

def tous_les_utilisateurs(request):
 
    # montrer 25 users par page
    paginator = Paginator(User.objects.all(), 25)
 
    # récupérer la page en cours
    page = request.GET.get('page')
    try:
        users = paginator.page(page)
    except PageNotAnInteger:
        # si la page n'est pas un entier, on affiche la page un
        users = paginator.page(1)
    except EmptyPage:
        # Si la page déborder, on affiche la dernière page
        users = paginator.page(paginator.num_pages)
 
    context = {"utilisateurs": users, "page": page}
 
    return render(request, "tous_les_utilisateur.html", context)

Et vous allez faire ça pour 512 vues. Des listings, c’est pas ce qui manque. Prendre un objet et afficher son détail, le modifier, vérifier si il existe, valider un formulaire, faire un listing d’objets, tout ça sont des tâches très courantes.

Pour cette raison, Django fournit des vues génériques, des fonctions NORMALES, mais qui sont paramétrables afin de pouvoir faire des listings, des validations de formulaire, etc, de tout et n’importe quoi. Sans avoir à écrire le code.

Les vues génériques sous forme de fonction

Une vue générique n’a rien de magique, c’est une vue ordinaire, seulement elle a des tas et des tas d’arguments pour pouvoir en faire ce qu’on veut. Chaque vue générique à un but précis. Par exemple, la vue générique object_list à pour but de créer des listings de n’importe quel objet, avec pagination.

Si on devait écrire notre vue précédente avec une vue générique, ça donnerait ça:

from django.views.generic.list_detail import object_list
 
def tous_les_utilisateurs(request):
    return object_list(request, queryset=User.objects.all(), paginate_by=25,
                       template_name="tous_les_utilisateur.html",        
                       template_object_name="utilisateurs")

object_list est une vue normale, donc elle attend en paramètre un objet request, et retourne une réponse. Ainsi, on lui passe request de tous_les_utilisateurs, et on récupère sa réponse, qu’on retourne comme si c’était la notre.

La différence, c’est que object_list à plein de paramètres en plus:

  • queryset est le paramètre qui dit quel objet lister;
  • template_name est le paramètre qui dit quel template utiliser;
  • template_object_name est le paramètre qui dit quel nom donner à la liste d’objets dans le template.

object_list s’occupe du reste, notamment de la pagination et de la gestion des erreurs. Elle frabrique l’objet context et le passe au template. Il n’y a donc plus qu’à écrire le template.

C’est d’ailleurs la partie difficile au début: comment on écrit le template ? En fait tout à fait normalement, il faut juste savoir quelles variables (et ce quelles contiennent) sont mises à disposition dans le template par la vue générique. Il faut lire la doc quoi :-) Ou faire comme moi et utiliser django-template-repl qui est une sorte de pdb pour template.

Dans notre cas, le template va contenir la variable pag_obj (qui est l’objet de pagination) et la variables utilisateurs qui est une liste d’objets User.

Les vues génériques suivent des conventions de nommage, et si on s’y tient, on peut même raccourcir le code encore plus:

def tous_les_utilisateurs(request):
    return object_list(request, queryset=User.objects.all(), paginate_by=25)

Mais dans ce cas il faut nommer obligatoirement notre template [app_label]/[model_name]_list.html (dans notre casmonapp/user_list.html) et dans ce template, récupérer la liste d’utilisateurs dans la variable de nom object_list (facile à retenir, c’est le même nom que la vue).

Comme toutes les vues génériques, object_list est très paramétrable. Supposons qu’on veuille rajouter la date du jour dans la page, il suffit de le rajouter dans le template et de faire ceci:

def tous_les_utilisateurs(request):
    aujourdhui = datetime.datetime.now()
    return object_list(request, queryset=User.objects.all(),
                       template_name="tous_les_utilisateur.html",
                       paginate_by=25,
                       template_object_name="utilisateurs",
                       extra_context={'date_du_jour': aujourdhui})

Et le template aura automatiquement accès à la variable date_du_jour.

Les vues génériques font gagner beaucoup de temps, mais elles font un peu peur au début car on ne sait pas trop ce qu’elles font à l’intérieur, ni comment faire le template.

Pour cette raison, je ne recommande pas aux débutants de les utiliser, faites des vues à la main d’abord, et quand vous aurez assez de code derrière vous pour comprendre les points communs entre toutes ces vues, vous saurez exactement ce que fait une vue générique: ce que vous avez réécrit 100 fois.

En cas de doute, la doc de Django explique comment utiliser les vues génériques, et surtout, une liste des vues génériques disponibles pour chaque cas d’utilisation.

Parmi les vues génériques les plus utiles:

  • object_list: lister un objet et paginer la liste
  • direct_to_template: retourner juste le template sans rien d’autre
  • redirect_to: une redirection pure et simple
  • object_detail: afficher un objet en particulier, ou une 404 si il n’existe pas
  • create_object: afficher un formulaire pour créer une objet
  • update_object: afficher un formulaire pour modifier un objet
  • delete_object: supprimer un objet, avec une confirmation

Rappelez-vous, les vues génériques sont des vues normales, et les vues sont des fonctions ordinnaires. La seule chose dont il faut se souvenir: elles acceptent en paramètre un objet Request, et retourne un objet Response. On peut donc faire tout ce qu’on fait d’habitude avec les fonctions: forcer des arguments, utilisers plusieurs vues dans une, faire des callbacks, etc.

Mais le plus important, on peut donc mettre une vue générique directement dans urls.py. Ainsi ceci:

from mon_app.views import tous_les_utilisateurs
urlpatterns = patterns('',
    url(r'^list/', tous_les_utilisateurs),
)

Peut tout à fait être remplacé par cela:

import datetime
from django.views.generic.list_detail import object_list
urlpatterns = patterns('',
    url(r'^list/', object_list, {"queryset": User.objects.all(),
                                 'template_name': 'tous_les_utilisateur',
                                 "template_object_name: "utilisateurs,
                                 "paginate_by": 25,
                                 extra_context={'date_du_jour': datetime.datetime.now})
)

object_list étant une vue normale, elle peut être appelée directement par urls.py. Il faut juste s’assurer de mettre le dictionnaire de paramètres additionels avec pour qu’elle soit configurée correctement. Du coup, on a zero code dans views.py.

On utilise rarement cela pour des vues comme object_list (ça pourrit un peut le urls.py qui doit rester facile à lire). Par contre, les vues direct_to_template et redirect_to sont très souvent utilisées de cette manière.

En conclusion, notez que vous pouvez très bien créer vos propres vues génériques. Une vue générique est juste une vue normale avec plein de paramètres pour qu’elle soit très souple et réutilisable.

Les vues génériques sous forme de classes

Les class base views, ou CBV, sont exactement la même chose que précédément, mais sous forme de classe. Elles se configurent plus de manière déclarative. Ainsi notre exemple précédent se ferait ainsi:

from django.views.generic import ListView
from django.contrib.auth.models import User
class TousLesUtilisateurs(ListView):
   context_object_name = "utilisateur"
   queryset = User.objects.all()
   template_name = "tous_les_utilisateurs.html"

Et dans urls.py:

from mon_app.views import TousLesUtilisateurs
urlpatterns = patterns('',
    url(r'^list/', TousLesUtilisateurs.as_view()),
)

Néanmoins, quand il faut rajouter des valeurs dans le context, ça se gate. En effet, une classe, c’est de l’objet, qui dit objet, dit (sauf prototypage) héritage, et héritage, dit overriding.

Les class based views sont bien faites: tout leur comportement peut être configuré. Le problème, c’est que ça suppose que vous sachiez exactement comment elles marchent. Par exemple, pour rajouter un objet dans le context, il faut overrider la méthode get_context_data:

import datetime
 
from django.contrib.auth.models import User
from django.views.generic import ListView
 
class TousLesUtilisateurs(ListView):
   context_object_name = "utilisateur"
   queryset = User.object.all()
   template_name = "tous_les_utilisateurs.html"
 
   def get_context_data(self, **kwargs):
        # qui dit overriding, dit appel de la méthode parent...
        context = super(TousLesUtilisateurs, self).get_context_data(**kwargs)
        # et on rajoute la date du jour dans le context
        context['aujourdhui'] = datetime.datetime.now()
        # le context retourner sera automatiquement injecté dans le template
        # dans la méthode render(), que vous ne voyez pas...
        return context

L’idée derrière ces classes, c’est que vous pouvez réutiliser du code bien plus facilement: on peut faire hériter des vues les unes des autres, et overrider seulement certaines méthodes. D’ailleurs, elles sont très bien pensées, et il y a des hooks partout.

Par exemple, si vous voulez tous les users, vous pouvez faire:

class TousLesUtilisateurs(ListView):
   model = User

Si vous voulez un filtrage particulier, vous pouvez faire:

class TousLesUtilisateurs(ListView):
   querset = User.object.filter(truc=machin)

Et si vous voulez un filtrage dynamique vous pouvez faire:

class TousLesUtilisateurs(ListView):
 
    def get_queryset(self):
        """
            Listing de tous les users, ou seulement de ceux qui ont accès
            à l'admin Django
        """
        # self.args[0] suppose que l'url prend un paramètre, ce que nous 
        # n'avons pas fait.  C'est pour l'exemple.
        if self.args[0] == "staff":
            return User.objects.filter(is_staff=True)
        return User.object.all()

Et la chaîne logique c’est: get_queryset est appelé automatiquement dans get_context_data, si c’est le votre, il override, sinon il essaye cls.queryset, et si il n’existe pas, il essaye de créer le queryset à partir de cls.model. Et sinon il fait une erreur.

C’est logique. Certes.

Mais c’est chiant.

Car c’est comme ça pour tout: il faut tout connaitre. Par coeur. Pareil pour les formulaires, les update des objets, etc. Et ensuite se rajoute la complexité des mixins. En plus la doc est super nulle sur ce point. Et il y a une sacrée liste de CBV.

Et franchement, le get_context_data, vous trouvez ça lisible ? Je vous garantie que quand on tombe sur une méthode de 10 lignes overridées d’une classe custom qui utilise 2 mixins sur un code qu’on a laissé depuis 3 mois, ça fait tout drôle.

Pour cette raison, je recommande ne ne PAS utiliser les CBV. Pour votre bien être, et celui de vos collègues. Certains de mes clients m’imposent d’ailleurs de ne pas le faire, contractuellement. Max en a horreur. Et après un an de mise en production de mes premières CBV, je confirme: le temps et la lisibilité perdus ne sont pas compensés par le gain en flexibilité.

Malheureusement les CBV sont la nouvelle manière de faire, les vues génériques sous forme de fonctions ont été marquées “deprecated”. Néanmoins, et tant que c’est possible, je recommande de continuer à les préférer aux CBV: comme les vues fonctions sont des vues et fonctions ordinnaires, il sera toujours facile de récupérer le code de Django qui font ces vues, même si elles sont retirées des version futures.

Explication du code

Bah, oui, parce qu’à la base, je devrais expliquer ce code, souvenez-vous…

Code 1

urlpatterns = patterns('',
    # On associe la vue "listview" à l'url "monsupersite.com/list/". Cette
    # association est appelée une route. Une route est donc une ligne dans
    # "urlpatterns" de urls.py.
    # {} est un dictionnaire de paramètres optionels
    # 'myobject_list' est le nom (appelé parfois "urlname") qu'on donne à
    # cette route
    (r'^list/', listview, {}, 'myobject_list'),
)

Avec ce model:

## models.py
from django.contrib.auth.models import User
 
# MyObject est un model tout simple avec une foreign key qui pointe vers
# le modèle User fournit par l'application "auth" de Django
# User est un modèle tout fait pour gérer les utilisateurs, mots de passes
# préférence, permissions, etc.
class MyObject(models.Model):
    ...
    author = models.ForeignKey(User)

Et cette vue:

## views.py
# on protège cette vue pour qu'elle ne soit accessible que pour les
# utilisateurs authentifiés
@login_required
def listview(request,
          queryset=MyObject.objects.all(),
          template_name='myproject/myobject_list.html'):
 
    # notez que l'auteur à ajouté des paramètres additionels à sa propre
    # vue: ils lui permettront d'avoir une vue plus souple et réutilisable
 
    # on récupère l'utilsiateur courant (request.user)
    # et on filtre MyObject.objects.all() pour qu'il ne contienne que les
    # objets qui soient pour cet utilisateurs
    qs = queryset.filter(author=request.user)
 
    # on retourne la réponse d'une vue générique qui va nous faire le listing
    # de ces objets
    return object_list(request, queryset=qs, template_name=template_name)
 
    # En gros, l'auteur à CREER lui même sa propre vue générique
    # car je le rappelle, une vue générique est une vue normale (et une fonction
    # normale), mais qui a plein de paramètres pour la rendre souple et
    # réutilisable.
    # Sa vue générique à pour but de faire un listing d'objets appartenant
    # à l'utilisateur courant. Il délègue quand même le gros
    # du boulot à une vue générique de Django, parceque faut pas déconner, hein...

La vue peut être traduite ainsi en mode vue générique:

## views.py
 
class ListView(generic.ListView):
    queryset = MyObject.objects.all()
    template_name = "myproject/myobjects_list.html"
 
    def get_queryset(self):
        # c'est ici qu'on fait le filtre par l'utilisateur courant
        return self.queryset.filter(author=self.request.user)
 
# comme un décorateur ne fonctionne pas sur une classe, cette astuce
# permet de récupérer l'équivalent d'une vue wrappées et importable directement
# dans urls.py
listview = login_required(ListView.as_view())

Et c’est une bonne illustration de ce qui est relou avec les vues génériques:

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())

VERSUS

@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)

Pour avoir les mêmes fonctionalités. Le bénéfice de l’un sur l’autre n’est pas énorme (il se trouve dans la réutilisabilité, dans des cas très poussés). Et il faut apprendre tout l’API des CBV pour faire la première.

Mais surtout, on perd le potentiel KISS dans le premier cas, car soyons franc, le plus souvent on a juste bsoin de ça:

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

Et ça, dans 3 mois, je le comprends tout de suite. Ca prends moins de place dans mon fichier. Et Max ne m’envoie pas de mails d’insultes.

Code 2

Routing:

## urls.py
urlpatterns = patterns('',
    (r'^list/', listview, {}, 'myobject_list'),
    # on rajoute une chtite vue pour créer une objet
    (r'^create/', createview, {}, 'myobject_create'),
)

Formulaire:

## forms.py
# on créé un formulaire à partir du model MyObject
# ce  formulaire permettra donc de créer une objet MyObject
class MyObjectForm(ModelForm):
    class Meta:
        model = MyObject
        exclude = ('author',)
 
    # le dev a ici choisi d'excluse le champ 'author' du formulaire
    # il veut en effet passer l'utilisateur en cours à la sauvegarde
    # afin que l'objet créé ait toujours pour autheur l'utilsateur courrant
    def save(self, user=None):
        # ici rien de fou, on fait un override du save, on appel le parent
        # et l'objet est créé. Amen.
        myobject = super(MyObjectForm, self).save(commit=False)
        myobject.author = user
        myobject.save()

Vue:

## views.py
from myproject.forms import MyObjectForm
 
# voici une vue normale, faite à la main
# elle elle réservée aux utilisateurs authentifiés
# mais accepte un paramètre pour lui dire sur quel formulaire travailler
# c'est donc encure une fois une vue générique faite à la mano
# puisque je vous le rappelle... Non, je déconne.
@login_required
def createview(request, form_class=MyObjectForm):
 
    # on check si la requête est une requête POST
    if request.method == 'POST':
        # si oui on prend les paramètres de la requête
        # et on les passe au formulaire
        # puis on vérifie si le formulaire est valide (pas d'erreurs de saisie)
        form = form_class(request.POST)
        if form.is_valid():
            # dans ce cas: on sauvegarde le formulaire en lui passant l'utilisateur
            # courant: un objet MyObjet est créé avec pour auteur
            # l'utilisateur courant
            myobject = form.save(user=request.user)
            # et on redirige sur la page décrivant l'objet
            # Ne cherchez pas comment il obtient ceci,
            # ce n'est pas expliqué dans son code surement volontairement
            # pour simplifier l'article, ce qui n'est pas plus mal
            return HttpResponseRedirect(myobject.get_absolute_url())
 
        # si le formulaire n'est pas valide, render_to_response
        # contiendra le formulaire avec les erreurs
    else:
        # si ce n'est pas une requête POST, on créé juste un formulaire
        # vierge à afficher
        form = form_class()
 
    # on retourne une réponse normale, avec le formulaire dans le context
    # et vous pouvez ignorer RequestContext, ça n'a pas d'importance pour nous
    # ici
    return render_to_response('myproject/myobject_form.html',
                              {'form': form},
                              context_instance=RequestContext(request))

Vous noterez que la vue précédent n’utilise pas de vue générique Django pour déléguer le boulot, ce qui explique qu’elle est longue.

Et voilà à quoi ressemblerait la vue en version CBV :

## views.py

from myproject.forms import MyObjectForm
 
class CreateView(generic.CreateView):
    form_class = MyObjectForm
    template_name = "myproject/myobject_form.html"
 
    # tous les classes génériques ont des hooks différent
    # ici on étend la CreateView, qui a a une méthode spécialement concue
    # pour la validation de formulaire
    # on l'override pour sauvegarder le formulaire en passant le user courant
    def form_valid(self, form):
        self.object = form.save(user=self.request.user)
        return super(CreateView, self).form_valid(form)
 
# idem que précédement
createview = login_required(CreateView.as_view())

11 thoughts on “Des vues normales aux vues génériques Django

  • Sam Post author

    Oh, j’ai oublié de le préciser ici, mais on utilise la nomenclature Django qui n’est pas du MVC. Donc les vues sont là où les plupart des gens attendent un controleur. Ce n’est pas une erreur.

  • Guts

    Pfiou bel article !
    Je finirai de le lire plus tard.
    Un truc qui manque pas mal et qui saute aux yeux sur votre blog, ce serait un glossaire des termes python, des termes de programmation plus généraux et surtout des termes à la con (TALC), etc.
    Histoire que tout le monde comprenne pour le mieux et que ce soit open même pour les débutants :)

  • Sam Post author

    Pas faux. Il manque pas mal de listing comme ça:

    – des termes techniques
    – des design pattern
    – des ressources d’apprentissage/information python/django
    – des libs utiles python/django

  • Luigi

    En plus la doc est super nulle
    parcequ’à la base => parce qu’à
    Max ne m’envois => ne m’envoie

  • IxDay

    Juste en passant pour confirmer que la CBV c’est de la merde (il n’y a pas d’autre mot). Je suis sur un projet où on n’utilise que ça. L’arbre d’héritage est infernal, le context est inaccessible car j’ai des CBV dans des CBV. Je déconseille totalement l’utilisation de cette abomination. J’ai l’impression de faire du JAVA dans une grosse SSII, les mecs veulent faire générique et bilan c’est tout le contraire. Je ne peux m’empecher de penser à la célébre phrase qui dit que la généricité c’est comme le sexe à l’adolescence, soit c’est un mensonge soit c’est mal fait (ça marche aussi pour le modulaire, le big-data et tout un tas de conneries de sous architecte logiciel qui n’ont jamais vu une ligne de code de leur vie).

    Sur ce j’espère que mon ton hargneux ne sera pas pris pour de la mauvaise foi et que vous ferez tout pour ne pas utiliser ce truc là.

  • vohu

    Yoo, attention dans la présentation des CVB y a une coquille :

    queryset = User.object.all()

    Il manque un ‘s’ à object…

Leave a comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.