Five class-based views everyone has written by now

30
Five class-based views everyone has written by now James Aylett artfinder.com Wednesday, 2 November 11 Django 1.3, released in March, introduced CLASS BASED VIEWS, which are intended to be make writing views easy and delightful. They replaced the function-based generic views, and therefore must be better, or something.

description

 

Transcript of Five class-based views everyone has written by now

Page 1: Five class-based views everyone has written by now

Five class-based viewseveryone has written

by now

James Aylettartfinder.com

Wednesday, 2 November 11

Django 1.3, released in March, introduced CLASS BASED VIEWS, which are intended to be make writing views easy and delightful. They replaced the function-based generic views, and therefore must be better, or something.

Page 2: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  AWub(DetailView):    model  =  Wub

class  Wubs(ListView):    model  =  Wub

Wednesday, 2 November 11

We’ll pretend you can’t pass parameters in urlconfs, because that’s icky and also misses the point.

Page 3: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  AWub(DetailView):    model  =  Wub

class  Wubs(ListView):    model  =  Wub

   def  get_queryset(self):        return  Wub.objects.exclude(            hidden=True        )

Wednesday, 2 November 11

The point is that you can override small bits of the ways the views work, to refine them for your application. Add further context for your templates, change the queryset and so on. There are conventional places to put templates, and for naming template context members. Upshot: little code, big effect.

Page 4: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  NewWub(CreateView):    model  =  Wub

class  DeleteWub(DeleteView):    model  =  Wub

class  UpdateWub(UpdateView):    model  =  Wub

Wednesday, 2 November 11

Generic views had ways of editing, creating and deleting, so you get that too. Write a suitable template, and EVERYTHING else is automatic, using a default form. If you want a different form, you can override that easily enough. Sounds good, right?

Page 5: Five class-based views everyone has written by now

Wednesday, 2 November 11

Unfortunately you are about to enter a world of pain. Let’s consider an example that isn’t completely trivial.

Page 6: Five class-based views everyone has written by now

from  django.db  import  models

class  WubFlock(models.Model):    name  =  models.CharField(…)

class  Wub(models.Model):    name  =  models.CharField(…)    flock  =  models.ForeignKey(        WubFlock,  related_name=‘wubs’)

Wednesday, 2 November 11

Okay, so we have a flock of Wubs. We’re building a wub management interface, so we’ll need to create new wubs within a flock. Except…oh no. You can’t do that.

Page 7: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  NewFlockWub(CreateView):    model  =  Wub

   #  what  goes  here?

Wednesday, 2 November 11

Okay, so you want to customise the form so it’ll exclude the “flock” field, but set it pre-save to the flock this wub is going to be in. (Yes you could have a dropdown of all flocks, but then your designer will murder you in your sleep.)

Page 8: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  NewFlockWub(CreateView):    model  =  Wub

   form_class  =  \        SomethingSomethingForm

Wednesday, 2 November 11

Okay, so we’ll define the form somewhere. Only…

Page 9: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  NewFlockWub(CreateView):    model  =  Wub

   form_class  =  \        SomethingSomethingForm

   #  erm,  but  we  need  to  get    #  the  flock  object  for  the  form

Wednesday, 2 November 11

You want to figure out the flock from the URL, say “/flock/my-awesome-wubs”. DetailView does this, and like all class-based views is built up of composable little pieces, so you could bring in SingleObjectMixin, which provides get_object to do this. Then we could override get_form_class to set up the form dynamically. But this behaviour is generic, so…

Page 10: Five class-based views everyone has written by now

from  moreviews  import  *

class  NewFlockWub(BoundCreateView):    model  =  Wub    bound_field  =  ‘flock’    queryset  =  WubFlock.objects.all()

Wednesday, 2 November 11

If your BoundCreateView isn’t this easy, you’re doing it wrong. “Bound” because the Wub is bound to the WubFlock.

Page 11: Five class-based views everyone has written by now

class  DeleteWub(DeleteView):    model  =  Wub

class  F(forms.ModelForm):    class  Meta:        model  =  Wub        exclude  =  (‘flock’,)

class  UpdateWub(UpdateView):    model  =  Wub    form_class  =  F

Wednesday, 2 November 11

We don’t have to worry about binding for deleting, and for updating we just ensure we don’t change the “flock” field.

Page 12: Five class-based views everyone has written by now

Wednesday, 2 November 11

But what about the WubFlock? We should be able to create it AND ITS WUBS in one go. In traditional function views you’d do this with forms and formsets within the same request. So we want to do the same thing in class-based views.

Page 13: Five class-based views everyone has written by now

class  ProcessMultipleFormsMixin:    """Modify  GET  and  POST  behaviour  to  construct  and  process  multiple  forms  in  one  go.  There's  always  a  primary  form,  which  is  a  ModelForm.

By  the  time  secondary  forms  are  saved,  self.new_object  on  the  view  will  contain  the  primary  object,  ie  the  object  that  the  primary  form  operates  on."""

Wednesday, 2 November 11

This isn’t one of the five classes, this is just a mixin. It’s not named perfectly, because although it DOES process multiple forms at once, it assumes one is the main form. This allows us to save the PRINCIPAL object, and then leaves a reference for all the other forms to use.

Page 14: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  NewFlock(MultiCreateView):    model  =  WubFlock    forms_models  =  [        {            ‘model’:  Wub,            ‘extra’:  1,        }    ]

Wednesday, 2 November 11

This syntax is a little opaque, but it didn’t seem worth creating yet more classes just as helpers when we have lists and dictionaries. The exclude of the project field in the ModelForm for making little Wubs is taken care of for you.

Page 15: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  UpdateFlock(MultiUpdateView):    model  =  WubFlock    forms_models  =  [        {            ‘model’:  Wub,            ‘extra’:  1,            ‘can_delete’:  True,        }    ]

Wednesday, 2 November 11

extra and can_delete here are both passed through to modelformset_factory. You can also set form inside the dictionary so you don’t just get a default ModelForm but can customise to your heart’s content.

Page 16: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  UpdateFlock(MultiUpdateView):    …    def  get_forms(self):        class  WubInlineForm(ModelForm):            def  save(self):                #  perhaps  we  default  the                  #  name  based  on  the  flock?        self.forms_models[0][‘form’]  \            =  WubInlineForm        return  super(...)()

Wednesday, 2 November 11

You can even dynamically adjust things if you so choose. In fact, you can avoid forms_models by implementing get_forms directly if you prefer.

Page 17: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  DeleteFlock(DeleteView):    model  =  WubFlock

Wednesday, 2 November 11

And of course deleting a flock will delete its wubs via the evil of the ORM’s implementing CASCADE DELETE itself.

Page 18: Five class-based views everyone has written by now

Wednesday, 2 November 11

There’s also a variant which will allow you to create an object bound to another but which itself has children bound to it. It’s imaginatively called MultiBoundCreateView. That’s not one of the five, that’s a bonus one. However now we need to think about non-editing views again, because we’ve still got a problem.

Page 19: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  Wubs(ListView):    model  =  Wub

class  Flock(DetailView):    model  =  WubFlock

Wednesday, 2 November 11

This is fine until you have a flock with three thousand wubs in. And wubs are very gregarious.

Page 20: Five class-based views everyone has written by now

from  django.views.generic  import  *

class  Wubs(ListView):    model  =  Wub    paginate_by  =  10

class  Flock(DetailView):    model  =  WubFlock

Wednesday, 2 November 11

ListView has pagination built in. Wouldn’t it be nice if we could do that for the CHILDREN bound to the object in a DetailView?

Page 21: Five class-based views everyone has written by now

from  django.views.generic  import  *from  moreviews  import  *

class  Wubs(ListView):    model  =  Wub    paginate_by  =  10

class  Flock(DetailListView):    model  =  WubFlock    paginate_by  =  10

Wednesday, 2 November 11

This is the fourth view. Warning: this exists, deep within the Artfinder codebase, but isn’t re-usable yet.

Page 22: Five class-based views everyone has written by now

Wednesday, 2 November 11

We’re not finished yet.

Page 23: Five class-based views everyone has written by now

From:  Your  Boss  <[email protected]>To:  You  <[email protected]>Subject:  Permissions

Yo  dawg.  I  was  over  at  Big  Client  Co  this  morning  and  noticed  that  they’re  able  to  edit  the  WubFlock  for  the  Sinister  Government  Agency.  This  contains  proprietary  Wub  technology,  and  geese,  so  THIS  SHOULD  NOT  BE  ALLOWED.

Wednesday, 2 November 11

Maybe your site allows everyone to see and do everything. Most don’t. What you used to do with function views was to check the permission at the top of the view and return a 403. In class-based views you need to do that around get_object, which gives you direct access to the object.

Page 24: Five class-based views everyone has written by now

def  editable():    def  decorator(fn):        @wraps(fn,  assigned=available_attrs(fn))        def  _wrapped_fn(self,  *args,  **kwargs):            obj  =  fn(self,  *args,  **kwargs)            if  obj.editable(self.request.user):                return  obj            else:                raise  HttpForbidden()        return  _wrapped_fn    return  decorator

class  _UpdateFlock(UpdateFlock):    get_object  =  \        editable(UpdateFlock.get_object)

Wednesday, 2 November 11

Oh god oh god I want to die. Ignore that decorators make most people’s heads hurt, just LOOK AT THAT CODE AT THE BOTTOM.

Page 25: Five class-based views everyone has written by now

class  _UpdateFlock(UpdateFlock):

   get_object  =  \        editable(            UpdateFlock.get_object        )

Wednesday, 2 November 11

There is no way this is going to look nice, no matter how many lines we wrap it onto. So this is the fifth class…that I’m really hoping someone has written. I want to write something like the following.

Page 26: Five class-based views everyone has written by now

class  _UpdateFlock(UpdateFlock):

   def  allowed(self):        return  self.object.editable(            self.request.user        )

Wednesday, 2 November 11

Which is basically the same but not ugly. I guess what we actually want here is a permissions-activating mixin.

Page 27: Five class-based views everyone has written by now

class  _UpdateFlock(    UpdateFlock,    SingleObjectPermissionsMixin):

   def  allowed(self):        return  self.object.editable(            self.request.user        )

Wednesday, 2 November 11

Page 28: Five class-based views everyone has written by now

class  SingleObjectPermissionsMixin(    object):    def  get_object(self):        obj  =  super(            SingleObjectPermissionsMixin,            self).get_object()        if  not  self.allowed(obj):            raise  HttpForbidden()        return  obj

   def  allowed(self):        raise  NotImplementedError()

Wednesday, 2 November 11

So this would be the fifth class. I haven’t written this, but I assume someone has. I suspect as I have it here, there are lots of problems, not least that HttpForbidden doesn’t exist in Django itself and so you’re dependent on something else to not only provide it but catch it in middleware and do something sensible with it.

Page 29: Five class-based views everyone has written by now

Summary

• BoundCreateView for making Wubs

• MultiCreateView for making WubFlocks

• MultiUpdateView to update WubFlocks

• DetailListView for paginating child objects

• SingleObjectPermissionsMixin is mythical

Wednesday, 2 November 11

The first three are written, but Ben caught me by surprise by asking me to talk, so they aren’t packaged and they’re not directly tested. DetailListView needs extracting from well-used internal code. The permissions stuff is entirely speculative; maybe we don’t need it.

Page 30: Five class-based views everyone has written by now

Questions?

• James Aylett

• @jaylett

• artfinder.com

• http://bit.ly/djugl-art

Wednesday, 2 November 11