Introducing the Analogic framework for business planning applications
Five class-based views everyone has written by now
1. 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.
2. 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.
3. 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.
4. 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?
5. Wednesday, 2 November 11
Unfortunately you are about to enter a world of pain. Let’s consider an example that isn’t
completely trivial.
6. 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.
7. 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.)
8. 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…
9. 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…
10. 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.
11. 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.
12. 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.
13. 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.
14. 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.
15. 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.
16. 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.
17. 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.
18. 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.
19. 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.
20. 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?
21. 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.
23. From:
Your
Boss
<phb@myco.co.com>
To:
You
<peon@myco.co.com>
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.
24. 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.
25. 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.
26. 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.
27. class
_UpdateFlock(
UpdateFlock,
SingleObjectPermissionsMixin):
def
allowed(self):
return
self.object.editable(
self.request.user
)
Wednesday, 2 November 11
28. 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.
29. 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.