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/:
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()) |
Une vue, c’est une fonction ORDIN
NAIRE qui prends une requête en paramètre et quiaccepterenvoie une réponse.Merci
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.
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 :)
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
En plus la doc est super nulle
parcequ’à la base => parce qu’à
Max ne m’envois => ne m’envoie
Notez combien il est difficile d’etre credible avec une mascotte pareille ;-) trop fort !!!!
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à.
http://sametmax.com/des-annees-plus-tards-je-naime-toujours-pas-les-cbv/
Yoo, attention dans la présentation des CVB y a une coquille :
queryset = User.object.all()
Il manque un ‘s’ à object…
Merci. Corrigé.
Bonjour.
Je viens de lire avec grand intérêt votre article. c’est vraiment très clairement écrit dans un style compréhensif. Je l’ai lu aussi avec grand espoir pour résoudre mon problème.
Malheureusement, je viens de constater que update_object n’est plus dans la version 1.11 de Django.
Comment faire alors avec django 1.11 ? Est-ce que ça veut dire que je n’ai pas d’autres choix que d’utiliser les vues bénériques basées sur des classes ?
Yep.