cbv – Sam & Max http://sametmax.com Du code, du cul Tue, 10 Sep 2019 09:14:50 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Histoire de ne pas perdre le fil : TrackingFields http://sametmax.com/histoire-de-ne-pas-perdre-le-fil-trackingfields/ http://sametmax.com/histoire-de-ne-pas-perdre-le-fil-trackingfields/#comments Sun, 07 Feb 2016 11:03:06 +0000 http://sametmax.com/?p=18067 Ceci est un post invité de Foxmask posté sous licence creative common 3.0 unported.

Préambule

Je sais bien qu’une partie de ce billet ne plaira pas à Sam&Max (thanks to the Django CBV & Mixin:)

Introduction
Le but du billet sera de montrer comment, sans rien changer dans un formulaire, on peut arriver à pister les modifications des données effectuées dans l’application.

La première partie va planter le décor en commençant par vous montrer comment s’articule une application avec formulaire composé d’un sous formulaire en sus (j’expliquerai pourquoi après :)

Pour ce faire, je vous emmène dans l’univers du 7° art, viendez on va refaire StarWars!

Un modèle, un formulaire, une vue, un template et ca sera fini.

le models.py

    from django.db import models


    class Movie(models.Model):
        """
            Movie
        """
        name = models.CharField(max_length=200, unique=True)
        description = models.CharField(max_length=200)

        def __str__(self):
            return "%s" % self.name


    class Episode(models.Model):
        """
           Episode - for Trilogy and So on ;)
        """
        name = models.CharField(max_length=200)
        scenario = models.TextField()
        movie = models.ForeignKey(Movie)

        def __str__(self):
            return "%s" % self.name

le forms.py, tout rikiki :

    from django import forms
    from django.forms.models import inlineformset_factory

    from starwars.models import Movie, Episode


    class MovieForm(forms.ModelForm):

        class Meta:
            """
                As I have to use : "exclude" or "fields"
                As I'm very lazy, I dont want to fill the list in the "fields"
                so I say that I just want to exclude ... nothing :P
            """
            model = Movie
            exclude = []

    # a formset based on the model of the Mother "Movie" and Child "Episode" + 1 new empty lines
    # for more details, have a look at https://docs.djangoproject.com/fr/1.9/topics/forms/modelforms/#inline-formsets
    EpisodeFormSet = inlineformset_factory(Movie, Episode, fields=('name', 'scenario'), extra=1)

la vue views.py, très sèche, très DRY ;)

    from django.http import HttpResponseRedirect
    from django.core.urlresolvers import reverse
    from django.views.generic import CreateView, UpdateView, ListView

    from starwars.models import Movie
    from starwars.forms import MovieForm, EpisodeFormSet


    class MovieMixin(object):
        model = Movie
        form_class = MovieForm

        def get_context_data(self, **kw):
            """ init form with data if any """
            context = super(MovieMixin, self).get_context_data(**kw)
            if self.request.POST:
                context['episode_form'] = EpisodeFormSet(self.request.POST)
            else:
                context['episode_form'] = EpisodeFormSet(instance=self.object)
            return context

        def get_success_url(self):
            """ where to go back, once data are validated """
            return reverse("home")

        def form_valid(self, form):
            """ form validation """
            formset = EpisodeFormSet((self.request.POST or None), instance=self.object)
            if formset.is_valid():
                self.object = form.save()
                formset.instance = self.object
                formset.save()

            return HttpResponseRedirect(reverse('home'))


    class Movies(ListView):
        model = Movie
        context_object_name = "movies"
        template_name = "base.html"


    class MovieCreate(MovieMixin, CreateView):
        """
            MovieMixin manages everything for me ...
        """
        pass


    class MovieUpdate(MovieMixin, UpdateView):
        """
            ... and as I'm DRY I wont repeat myself myself myself ;)
        """
        pass

Pour finir de planter le décors et les costumes (merci Roger Hart et Donald Cardwell)

le template base.html

    
    
    
        Manage stories for StarWars
    
    
    

Stories Manager for Starwars

{% block content %} Add a movie

Movie list

{% endblock %}

enfin movie_form.html (le template utilisé par les UpdateView & CreateView)

    {% extends "base.html" %}
    {% block content %}
    
{% csrf_token %} {{ formset.management_form }} {{ form.as_table }}
{{ episode_form.as_table }}
{% endblock %}

Mise à jour de la base de données

cela s’impose :

(starwars) foxmask@foxmask:~/DjangoVirtualEnv/starwars/starwars $  ./manage.py migrate

Operations to perform:
  Synchronize unmigrated apps: messages, starwars, staticfiles
  Apply all migrations: contenttypes, admin, sessions, auth
Synchronizing apps without migrations:
  Creating tables...
    Creating table starwars_movie
    Creating table starwars_episode
    Running deferred SQL...
  Installing custom SQL...

Voilà le tout est prêt (après le lancement du serveur bien sûr), je peux allégrement créer ma double trilogie pépère tel George Lucas.

Trackons l’impie

Seulement un jour arrive où, moi, George Lucas, je vends StarWars à Walt Disney, mais comme je ne veux pas rater de ce qu’ils vont faire de mon “bébé”, je rajoute un “tracker de modifications” à mon application, pour ne pas perdre le “field” de l’Histoire.

Installation de Tracking Fields

en prérequis django-tracking-fields requiert django-cuser, donc ze pip qui va bien donne :

    (starwars) foxmask@foxmask:~/DjangoVirtualEnv/starwars/starwars $ pip install django-tracking-fields django-cuser
    Collecting django-tracking-fields
      Downloading django-tracking-fields-1.0.6.tar.gz (58kB)
        100% |████████████████████████████████| 61kB 104kB/s 
    Collecting django-cuser
      Downloading django-cuser-2014.9.28.tar.gz
    Requirement already satisfied (use --upgrade to upgrade): Django>=1.5 in /home/foxmask/DjangoVirtualEnv/starwars/lib/python3.5/site-packages (from django-cuser)
    Installing collected packages: django-tracking-fields, django-cuser
      Running setup.py install for django-tracking-fields ... done
      Running setup.py install for django-cuser ... done
    Successfully installed django-cuser-2014.9.28 django-tracking-fields-1.0.6

comme de coutume, après un pip install, une modification dans le settings.py suit,

INSTALLED_APPS = (
        ...
        'cuser',
        'tracking_fields',
        ...
    )
    MIDDLEWARE_CLASSES = (
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
        'django.middleware.security.SecurityMiddleware',
        'cuser.middleware.CuserMiddleware',  ## <=== ne pas oublier, pour chopper le user qui fait le con avec mes films;)
    )

le petit migrate qui va bien aussi pour ajouter les tables pour les modèles de django-tracking-fields

    (starwars) foxmask@foxmask:~/DjangoVirtualEnv/starwars/starwars $  ./manage.py migrate
    Operations to perform:
      Synchronize unmigrated apps: staticfiles, messages, cuser, starwars
      Apply all migrations: auth, sessions, contenttypes, tracking_fields, admin
    Synchronizing apps without migrations:
      Creating tables...
        Running deferred SQL...
      Installing custom SQL...
    Running migrations:
      Rendering model states... DONE
      Applying tracking_fields.0001_initial... OK
      Applying tracking_fields.0002_auto_20160203_1048... OK

et nous voilà prêts à joueur les trackers.

Utilisation

On ne peut pas rêver plus simple, cela se résume à un décorateur sur le modèle qui identifie quelles données sont modifiées, et un field histo qui va lier le modele TrackingEvent de l'application TrackingFields, à ma table à surveiller. Et là, bien que le modele ait été modifié, inutile de faire un nouveau python manage.py migrate, rien ne bougera, histo sera une GenericRelation(). En effet, TrackingEvent repose sur ContenType aka "L'Infrastructure des Types de Contenu". Si vous avez déjà tripoté la gestion des permissions, vous avez déjà dû vous y frotter;)

Pour la faire courte, en clair ça donne :

le models.py arrangé pour l'occasion du décorateur

    from django.db import models
    from django.contrib.contenttypes.fields import GenericRelation

    from tracking_fields.decorators import track
    from tracking_fields.models import TrackingEvent


    @track('name', 'description')   # decorator added
    class Movie(models.Model):
        """
            Movie
        """
        name = models.CharField(max_length=200, unique=True)
        description = models.CharField(max_length=200)
        # to get the changes made on movie
        histo = GenericRelation(TrackingEvent, content_type_field='object_content_type')

        def episodes(self):
            return Episode.objects.filter(movie=self)

        def __str__(self):
            return "%s" % self.name

    @track('name', 'scenario')   # decorator added
    class Episode(models.Model):
        """
           Episode - for Trilogy and So on ;)
        """
        name = models.CharField(max_length=200)
        scenario = models.TextField()
        movie = models.ForeignKey(Movie)
        # to get the changes made on episode
        histo = GenericRelation(TrackingEvent, content_type_field='object_content_type')

        def __str__(self):
            return "%s" % self.name

bon là c'est simplissime comme une recette de pate à crêpes: 3 imports de rigueur, le décorateur et la GenericRelation() on mélange le tout et ca donne ce qui suit J'ai, au passage, rajouté une fonction episodes à ma classe Movie, dont je vous reparlerai plus bas.

le template de la DetailView (pour afficher uniquement les details d'un film) qui va bien

    
    {% for h in object.histo.all %}
       {% for f in h.fields.all %}
           
       {% endfor %}
    {% endfor %}
       
History of the modification of {{ object }}
Old ValueNew ValueByat
{{ f.old_value }}{{ f.new_value }}{{ h.user }}{{ h.date }}

A présent si je me rends dans ma page pour modifier le scénario d'un Episode, mon template ci dessus, ne m'affichera pas ces modications ! Pourquoi bou diou ? Parce qu'ici j'affiche "l'histo" de Movie pas de Episode... On comprend à présent ici mon intéret pour le sous formulaire. Le "problème" aurait été masqué si je m'étais arrêté à un seul simple formulaire.

Corrigeons

c'est là qu'entre en jeu la fonction episodes à ma classe Movie, pour me permettre d'itérer dessus et afficher tout le toutim

le template de la DetailView qui va bien (bis)

    
    {% for h in object.histo.all %}
       {% for f in h.fields.all %}
           
       {% endfor %}
    {% endfor %}
        
History of the modifications of {{ object }}
Old ValueNew ValueByat
{{ f.old_value }}{{ f.new_value }}{{ h.user }}{{ h.date }}
{% for ep in object.episodes %} {% if ep.histo.all %} {% for h in ep.histo.all %} {% for f in h.fields.all %} {% if f.old_value == f.new_value %} {# they are the same when the new value is created to avoid to display "null" #} {% else %} {% endif %} {% endfor %} {% endfor %}
history of the modifications of Episode
Old ValueNew ValueByat
{{ f.old_value }}{{ f.new_value }}{{ h.user }}{{ h.date }}
{% endif %} {% endfor %}

Voili voilou ! Et en prime, si vous êtes curieux, coté admin, vous avez aussi la liste de toutes les modifications si besoin ;)

Aux utilisateurs avertis qui diraient :

pourquoi l'avoir recodé coté front puisque c'est déjà géré coté admin sans lever le petit doigt ?

Parce que George Lucas veut montrer les modifications apportées à son bébé StarWars par Walt Disney, au monde entier pardi !

Ah un détail en passant : dans l'admin la vue qui affiche la liste des modifications donne : "Episode Object" ou "Movie Object". Pour éviter ça, zavez dû remarquer que j'ai mis la fonction __str__ dans mes modèles ce qui vous rendra une valeur plus "lisible" sur ce qui a été modifié.

Conclusion :

Dans la vraie vie de votre serviteur, ne se voyait pas créer un modele "history" lié "physiquement" par une FK à chaque modèle, entreprenait de chercher au travers de la toile quelques ressources.

C'est finallement sur #django-fr@freenode qu'il a posé la question et a obtenu de Gagaro le grââl : une application nommée tracking-fields, dont il est l'auteur.

Pour une fois qu'il fait sa faignasse en ne codant pas tout by himself, ça fait plaisir de tomber sur une appli pareille !

Si vous voulez jouer avec le code de ce gestionnaire de films c'est par ici la bonne soupe

]]>
http://sametmax.com/histoire-de-ne-pas-perdre-le-fil-trackingfields/feed/ 16 18067
Je n’expliquerai plus les CBV http://sametmax.com/je-nexpliquerai-plus-les-cbv/ http://sametmax.com/je-nexpliquerai-plus-les-cbv/#comments Mon, 11 May 2015 11:42:49 +0000 http://sametmax.com/?p=16221 Les Class Based Views sont des vues génériques réutilisables qui permettent de faire automatiquement des tâches courantes dans Django comme :

  • Récupérer et afficher une ou plusieurs entrées en base de données.
  • Afficher et valider un formulaire.
  • Faire un mix des deux pour modifier des entrées en base.

Je ne les ai jamais aimées. Avant, les vues génériques étaient sous forme de fonction, simples, pratiques, c’était parfait. Et ça a été retiré du framework pour des versions OO sous prétexte que c’était plus flexible.

Maintenant, ce sont ces gros tas immondes, avec un ordre d’appel de méthodes complexes, des mixins dans tous les sens et une chaine d’héritage velue.

Je pense que c’est une bonne occasion pour rappeler que la POO n’est pas faite pour être utilisée partout, tout le temps. Aucun putain de paradigme de programmation n’est fait pour être utilisé partout, tout le temps. C’est pour ça qu’il y en a plusieurs.

Dans notre cas des CBV, la conséquence est que c’est un enfer pour expliquer tout usage qui ne soit pas un hello world, ce que la doc se garde bien de faire.

Après il y a toujours un malin pour dire “mais si tu vois tu fais ça, et ça, ça prend 5 lignes, c’est pas si dur”. Ouai, maintenant va expliquer ça à un stagiaire, et donne lui un exercice avec une form view qui doit modifier un objet, le lier à un user seulement si il a la permission de le faire. Juste pour voir en combien de temps il trouve. Je vais en WE à Barcelone en attendant. Je prévoie une extension à Rome pour quand il devra relire son code le trimestre prochain.

Et si encore c’était pour un gain de productivité évident, je fermerais les yeux. Mais c’est pas le cas. Même moi qui sait utiliser ces… ces choses… je dois regarder dans la doc à chaque fois que j’en utilise une. Et ensuite quand je relis mon code quelques mois plus tard, je chope de pattes d’oies aux coins des yeux.

If you have to refer to the documentation every time you use a module, find (or build) a new module.

Dixit Kenneth Reitz, le mec qui a pondu requests. Donc on va user something else que les CBV, si vous le voulez bien.

Un lecteur m’a (encore) pointé du doigt qu’un schéma du MRO et de l’ordre d’appel des méthodes serait utile. Vous vous rendez compte du truc ? On peut comprendre l’ORM sans schéma alors que c’est une machine de guerre, et il faut une carte pour comprendre comment faire un listing un peu custo avec une CBV ? J’ai évalué la somme de travail pour faire ledit schéma, et elle est énorme : trop de classes, trop de méthodes, trop de subtilités.

Bref, je jette le tablier, fuck les CBV. Je ne les inclurai plus dans mes formations, mes tutos, mes aides sur les forums, etc.

]]>
http://sametmax.com/je-nexpliquerai-plus-les-cbv/feed/ 17 16221
Des années plus tard, je n’aime toujours pas les CBV http://sametmax.com/des-annees-plus-tards-je-naime-toujours-pas-les-cbv/ http://sametmax.com/des-annees-plus-tards-je-naime-toujours-pas-les-cbv/#comments Tue, 21 May 2013 08:52:09 +0000 http://sametmax.com/?p=5976 django-rest-framework, une très belle application, qui fait un usage utile et massif des CBV, et effectivement, cette architecture m'a rendu très productif. Mais je maintiens ce que je dis depuis le début : ce n'est pas un pas en avant.]]> Les Class Based Views se trouvent maintenant partout en Django. Je les comprends aujourd’hui très bien, et les utilise souvent au mieux. Dernièrement j’ai dû utiliser django-rest-framework, une très belle application, qui fait un usage utile et massif des CBV, et effectivement, cette architecture m’a rendu très productif.

Mais je maintiens ce que je dis depuis le début : ce n’est pas un pas en avant.

D’abord, qui dit CBV dit référence constante à la documentation. J’ai été productif avec CBV parce que :

  • Je connais l’API Django sur le bout des doigts.
  • Je comprends parfaitement les CBV (et la POO en Python en général).
  • Je lis très vite, et j’ai l’habitude de fouiller dans les docs.
  • Je me balade dans le code source naturellement.

Mais ce n’est pas comme ça que c’est censé être. Si vous utilisez une belle API, comme requests ou path.py, vous n’avez PAS besoin de tout cela. Vous jetez un coup d’œil à la doc, et vous faites 80% du boulot dans le shell, puis de temps à autre, vous vous référez à la documentation sur les points ambiguës. Je n’ai jamais du aller dans le code source de requests, je n’ai jamais eu besoin de comprendre urllib de fond en comble pour utiliser requests, et je n’ai pas besoin de connaître les subtilités de plusieurs paradigmes de programmation pour utiliser requests.

Alors oui, requests ne peut en aucun cas être comparé à Django en terme de taille et de couverture de besoin, et certes, en tant que professionnel averti, je suis tenu d’être capable de faire tout cela. Mais il y a quand même une énorme différence d’effort à fournir, et de qualification à avoir, pour l’usage de cette techno.

En prime, il est TRÈS difficile de débugger une CBV :

  • Dans une vue générique sous forme de fonction, on peut juste faire import ipdb, et faire un step puis débugger tranquillement. Dans une CBV, vous le mettez où le break point ? Soit vous overridez une méthode (et il faut savoir par cœur laquelle), soit vous faites un wrapper provisoir sous forme de fonction (!), soit vous mettez le break cash dans le code source de la lib. C’est nul à chier, et improductif. Et certainement pas à la portée d’un débutant.
  • Les mixins ont un tas de comportements implicites, qu’il faut connaître pour comprendre pourquoi ceci fait cela. Et comme il y a généralement 3 ou 4 mixins (d’ailleurs, combien de dev peuvent donner la définition d’un mixin ?), et bien si il y a un problème, il y a 4 sources possibles. Qui héritent parfois d’autres classes, ce qui démultiplie le truc.
  • Quand ça plante le stack trace est blindée de références à droite et à gauche. Un putain de labyrinthe.

Rajoutez à cela que la syntaxe est verbeuse comme c’est pas permis :

def get_context_data(self, *args, **kwargs):
    context = super(MaClass, self).get_context_data(*args, **kwargs)
    context['ma_var'] = "ma valeur"
    return context

Pour rajouter UNE variable dans le contexte ? Rhaaaaaaaaaaa. Je code en Python, pas en Java !

Et terminez sur le fait que pour utiliser tous les décorateurs codés depuis 5 ans, vous avez une syntaxe bien pourrie ou alors des réimplementations complètes sous forme de mixins. Yeah !

Vous comprendrez que ça me gonfle.

Tout ça pour quel gain ? Oui on gagne un peu en flexibilité, mais ça ne compense pas tout le reste, et ça ne permet en rien de faire des choses qu’on ne pouvait pas faire avant.

Je n’ai pas vu encore la moindre utilisation des CBV qui me fasse dire, “putain c’est tellement mieux qu’avant pour cette partie”. Ni aucune lib qui me fasse dire : “ouais, on aurait pas pu le faire aussi bien avec des vues sous forme de fonctions”.

Avec les CBV devenant la manière par défaut de coder les vues en Django, le framework devient beaucoup moins accessible au débutant, alors que sa facilité d’usage était un de ses points forts.

Bref, je suis de l’avis de Nick Coghlan, les CBV ne sont pas une erreur, elles ont leur utilité, mais balancer à la poubelle les vues sous forme de fonction est une vaste connerie.

]]>
http://sametmax.com/des-annees-plus-tards-je-naime-toujours-pas-les-cbv/feed/ 24 5976
Une alternative aux class base views en Django http://sametmax.com/une-alternative-aux-class-base-views-en-django/ http://sametmax.com/une-alternative-aux-class-base-views-en-django/#comments Sun, 26 Aug 2012 14:26:06 +0000 http://sametmax.com/?p=1898 Ce n’est pas un secret que nous n’aimons pas trop les CBV sur ce blog. Pourtant des tas de gens continuent de les utiliser car avoir des vues sous forme de classe leur permet plus de réutilisabilité.

Ce n’est pourtant pas nécessaire.

On vous a déjà dit qu’une vue Django n’était qu’une fonction ordinaire, qui prenait en paramètre une requête, et retournait une réponse.

On ne vous a pas tout dit.

En fait Django accepte n’importe quel callable comme vue, pas juste une fonction. Et Python permet de réagir à l’appel de n’importe quel objet grâce à la méthode __call__.

Du coup:

class MaVue(object):
    def __call__(self, request):
        # mettre ici le code de la vue
        # comme si c'était une vue foncton

ma_vue = MaVue()

Puis dans urls.py:

url('/', 'ma_vue')

Marche parfaitement.

Et vous pouvez hériter de MaVue, faire des mixins, et tout le toutim. Sans avoir à ajouter de la complexité, comme si c’était une vue fonction normale.

Il y a juste un truc à savoir: tout ce qu’on met dans self.attribut est partagé entre toutes les instances. Donc sur ce type de vue sous forme de classe, on évite de stocker un truc dans self, on fait des méthodes de classes auxquelles on passe des paramètres.

Sinon on évite juste d’utiliser les vues sous forme de classe. on vit très bien sans.

]]>
http://sametmax.com/une-alternative-aux-class-base-views-en-django/feed/ 7 1898
Explication de code: des mixins et des décorateurs de méthode pour Django http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/ http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/#comments Tue, 21 Aug 2012 14:37:57 +0000 http://sametmax.com/?p=1812 Suite à notre appel à l’envoi de code à expliquer, nous avons reçu ceci:

Pouvez-vous m’aider à comprendre ce code?

https://gist.github.com/3092600

La fonction dispatch, à quoi sert-elle?

L’appel via super, on appelle le parent de la classe?

Merci, je vous ferai une statue un jour!

Prépare le marbre.

Programmation orientée objet

Commençons par:

L’appel via super, on appelle le parent de la classe?

Oui, très exactement.

Et j’en profite pour rappeler que si vous utilisez les CBV, il va vous falloir vous toucher en programmation orientée objet: définition de classe, héritage multiple, overriding, et autres joyeusetés.

Comme ce genre d’infos peut faire l’objet d’une série d’articles à part entière, je vais être obligé de partir du principe que vous savez coder en objet pour expliquer ce code. Sinon, ce post ferait quelques milliers de lignes. Si ce n’est pas votre cas, lisez notre dossier sur la POO, et revenez après.

Worflow des CBV

La fonction dispatch, à quoi sert-elle?

Les CBV ont un ordre d’éxécution pour leurs méthodes: render_to_response() retourne la vue, mais elle est appelée depuis get() ou post(), qui appellent aussi get_context_data() pour créer le context du template. Et get_context_data() appelle get_query_set() pour les vues qui utilisent l’ORM. Il faudrait que je fasse un gros schéma de tout ça un jour.

Dispatch() est la méthode qui appelle toutes les autres. Elle choisi notament si on appelle la méthode get() ou post(). C’est en quelque sorte la méthode mère. Si on veut faire un truc avant que la vue ne travaille, c’est la dedans qu’il faut agir.

Les décorateurs de méthode

Comme vous l’avez vu, le code utilise @method_decorator. Encore une fois, je vais partir du principe que vous vous touchez avec les décorateurs, si ce n’est pas le cas, il y a un article pour ça™.

Néanmoins, dans l’article on ne parle que des décorateurs de fonctions. Comment décorer une méthode ? Et bien c’est pareil, sauf qu’il faut que votre décorateur accepte self en plus comme argument dans la fonction qu’il retourne.

Afin d’éviter de réécrire tous les décorateurs en double, on utilise @method_decorator qui est un décorateur… pour décorateurs ^^ Il transforme un décorateur pour fonction afin qu’il soit applicable à une méthode.

Ainsi quand vous voyez @method_decorator(login_required), ça veut dire “tranformer le décorateur login_required pour qu’il marche sur les méthodes, et l’apppliquer sur la méthode juste en dessous”

Les mixins

Je suis sûr que vous n’avez pas pu vous empêcher de vous demander, à la vue de ça:

LoginRequiredMixin(object):
    """
        View mixin which requires that the user is authenticated.
    """

Mais c’est quoi un mixin non de diou ?

Alors, déjà, on se rassure, ce n’est pas encore un nouveau truc compliqué à apprendre, c’est juste le nom d’un truc que vous connaissez déjà.

Un mixin, c’est le nom qu’on donne à une classe dont le but est exclusivement d’être utilisé pour l’héritage, afin de rajouter une fonctionalité à la classe enfant.

En gros, ce n’est qu’un nom, ça n’a rien de spécial, c’est une classe normale. Mixin est juste le rôle de la classe.

Les mixins ne sont PAS des interfaces, car ils font toujours quelque chose. Ils sont utilisés quand on a un code générique qu’on veut réutiliser dans plusieurs classes.

Les mixins changent le comportement, mais PAS la nature de la classe enfant. Il n’y a pas de AnimalMixing dont hériterait une class Chien, ou Chat. Ca c’est de l’héritage normal. On ne parle de mixin que pour le comportement.

Les mixins ne marchent que dans les langages qui autorisent l’héritage multiple, car on doit pouvoir hériter de plein de mixins d’un coup pour que ça soit utile.

Explication du premier Snippet

Vous vous souvenez du temps où c’était si simple de protéger une vue ?

@login_required
def ma_vue(request):

Avec les CBV, ce temps là est finit mes amis. C’est pour ça que je n’ai pas beaucoup d’amour pour elles.

Pour protéger une CBV on a plusieurs choix:

class MaView(ListView):
    ...

ma_vue = login_required(MaView.as_view())

Et on importe ma_vue dans urls.py.

OU directement dans urls.py:

...
url('/this/is/not/a/url', login_required(MaView.as_view()))
...

OU la méthode recommandée par les mecs de Django, méga simple et intuitive:

class MaView(ListView):
    ...
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(ProtectedView, self).dispatch(*args, **kwargs)

On applique le décorateur @login_required à la méthode dispatch(). Comme c’est la méthode qui appelle toutes les autres, la vue est ainsi protégée.

Seulement voilà, tout celà est bien relou, et l’auteur des snippets y remédie:

class LoginRequiredMixin(object):
    """
      View mixin which requires that the user is authenticated.
    """
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super(LoginRequiredMixin, self).dispatch(
            self, request, *args, **kwargs)

Ce faisant, il créé un mixin qui va appliquer ce décorateur. Mixin qu’on peut réutiliser ainsi:

class MaView(LoginRequiredMixin, ListView):
    ...

Et protéger une vue redevient simple à nouveau !

Attention, il faut bien mettre le mixin en premier dans la liste de parents. En effet, dispatch() de MaView va appeler celui de LoginRequiredMixin qui va appeler super() qui va va ainsi appeler dispatch() de ListView. C’est ce qu’on appelle le MRO (Method Résolution Order), l’ordre à vachement d’importance, et ça mériterait un article à lui tout seul.

Je résume:

dispatch() est la méthode d’un vue qui appelle toutes les autres. LoginRequiredMixin a sa méthode dispatch() protégée pour qu’elle ne soit accessible que par les utilisateurs enregistrés. MaView hérite du mixin pour intégrer cette fonctionalité. Du coup son dispatch() va utiliser le dispatch() de LoginRequiredMixin, qui est protégé. Comme LoginRequiredMixin est bien faites, son dispatch() appelle celui de ListView, et ainsi la vue fonctionne correctement.

Les autres snippets

On peut maintenant aller plus vite.

Le deuxième mixin permet de n’autoriser l’accès à une vue que si on a les permissions nécessaires (Django vient en effet avec toute une gestion des permissions dans son app contrib ‘auth’)

class PermissionsRequiredMixin(object):
    # les permissions nécessaires sont stockées dans cet attribut
    required_permissions = ()

    # dispatch est encore une fois protégée contre les users non loggés
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # on rajoute une subtilité:
        # on vérifie si le user a les permissions exigées avant
        # d'appler le dispatch() du parent
        # si on a pas les permissions, on rajoute un message d'erreur
        # et on redirige vers la page de login
        # sinon on appelle le dispatch() du parent comme d'hab
        if not request.user.has_perms(self.required_permissions):
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(PermissionsRequiredMixin, self).dispatch(
            request, *args, **kwargs)

Ca s’utilise comme ça:

class MaVue(PermissionsRequiredMixin, ListView):
    required_permissions = (
        'dealer.create_cocaine',
        'dealer.delete_cocaine',
    )

Un utilisateur qui n’a pas les permissions create_cocaine et delete_cocaine de l’app ‘dealer’ sera redirigé vers la page de login.

En effet, MaVue.dispatch appelle la méthode PermissionsRequiredMixin.dispatch, mais avec une subtilité: le self passé en paramètre est celui de MaVue. self.required_permissions dans PermissionsRequiredMixin.dispatch est donc en vérité MaVue.required_permissions. Relisez ce paragraphe plusieurs fois.

Notez en revanche que la page de redirection n’est pas configurable, ce qui est bien dommage. Ca se corrige facilement:

class PermissionsRequiredMixin(object):
    required_permissions = ()
    redirect_url = settings.LOGIN_URL # <== HOP

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        if not request.user.has_perms(self.required_permissions):
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(self.redirect_url) # <== HOP
        return super(PermissionsRequiredMixin, self).dispatch(
            request, *args, **kwargs)

Du coup on a plus de marge de manoeuvre.

Le 3eme et 4eme snippet, c'est la même chose. Kiff kiff. Pareil.

Mais au lieu de vérifier les permissions, on vérifie juste si l'utilisateur à accès à l'admin Django (is_staff) ou que l'utilisateur est un superutilisateur (is_superuser: il a toutes les permissions):

class StaffRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # j'ai pas besoin de vous expliquer ça quand même ?
        if not request.user.is_staff:
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(StaffRequiredMixin, self).dispatch(request,
            *args, **kwargs)

class SuperUserRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # je pense que c'est assez explicite
        if not request.user.is_superuser:
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(SuperUserRequiredMixin, self).dispatch(request,
            *args, **kwargs)

Comme je le disais plus haut, on peut utiliser plusieurs mixins en même temps. Par exemple, si je veux limiter l'accès à une vue aux dealers qui font partie de mon staff:

class MaVue(PermissionsRequiredMixin, StaffRequiredMixin, ListView):
    required_permissions = (
        'dealer.create_cocaine',
        'dealer.delete_cocaine',
    )

Notez encore une fois que les mixins sont là en premier, car MaVue.dispatch appelle PermissionsRequiredMixin.dispatch qui appelle StaffRequiredMixin.dispatch qui appelle ListView.dispatch.

En revanche, dans ce cas précis l'ordre des mixins, entre eux, n'a pas d'importance. Ce ne sera pas toujours le cas: il faut comprendre comment les mixins agissent pour savoir si certains doivent être mis en premier. C'est tout le problème des CBV: pour en faire un usage productif, la somme de choses à savoir est assez démente.

]]>
http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/feed/ 24 1812
Des vues normales aux vues génériques Django http://sametmax.com/des-vues-normales-aux-vues-generiques-django/ http://sametmax.com/des-vues-normales-aux-vues-generiques-django/#comments Wed, 15 Aug 2012 18:18:20 +0000 http://sametmax.com/?p=1715 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.]]> 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())
]]>
http://sametmax.com/des-vues-normales-aux-vues-generiques-django/feed/ 13 1715