Five class-based views everyone has written by now
-
Upload
james-aylett -
Category
Business
-
view
7.774 -
download
1
description
Transcript of 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.
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.
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.
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?
Wednesday, 2 November 11
Unfortunately you are about to enter a world of pain. Let’s consider an example that isn’t completely trivial.
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.
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.)
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…
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…
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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?
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.
Wednesday, 2 November 11
We’re not finished yet.
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.
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.
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.
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.
class _UpdateFlock( UpdateFlock, SingleObjectPermissionsMixin):
def allowed(self): return self.object.editable( self.request.user )
Wednesday, 2 November 11
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.
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.
Questions?
• James Aylett
• @jaylett
• artfinder.com
• http://bit.ly/djugl-art
Wednesday, 2 November 11