Class-based (generic) views in Django 1.3

by Łukasz Rekucki (lrekucki@gmail.com)

About Me

2004-2009:

Student at University of Warsaw (MISMaP): Computer Science and Mathematics.

Around 2008/2009

Got sucked into Django world (thanks to Marek Stępniowski) to work on open-source project Wolnelektury.pl.

March 2010

Joined a promising Polish start-up called Smartupz.com.

August 2010

My first patch got commited to Django code base. Yey!

Now

Still working at Smartupz. Trying to get more patches into Django so that my daily work is easier.

More Adverts ;)

Smartupz Logo

Make a quick note about who paid for the trip.

What is a view ?

view (n.):
Function that takes a Request together with arguments from URL resolver and returns a Response.
1 def article_list(request):
2     articles = Article.objects.all()
3     return render_to_response("article_list.html", {"articles": articles}, 
4                context_instance=RequestContext(request))

Talk about how to extend this view to be more reusable

Be DRY!

 1 def object_list(request, queryset, paginate_by=None, page=None,
 2         allow_empty=True, template_name=None, template_loader=loader,
 3         extra_context=None, context_processors=None, template_object_name='object',
 4         mimetype=None):
 5     if extra_context is None: extra_context = {}
 6     queryset = queryset._clone()
 7     if paginate_by:
 8         paginator = Paginator(queryset, paginate_by, allow_empty_first_page=allow_empty)
 9         if not page:
10             page = request.GET.get('page', 1)
11         try:
12             page_number = int(page)
13         except ValueError:
14             if page == 'last':
15                 page_number = paginator.num_pages
16             else:
17                 # Page is not 'last', nor can it be converted to an int.
18                 raise Http404
19         try:
20             page_obj = paginator.page(page_number)
21         except InvalidPage:
22             raise Http404
23         c = RequestContext(request, {
24             '%s_list' % template_object_name: page_obj.object_list,
25             'paginator': paginator,
26             'page_obj': page_obj,
27             'is_paginated': page_obj.has_other_pages(),
28         }, context_processors)
29     else:
30         c = RequestContext(request, {
31             '%s_list' % template_object_name: queryset,
32             'paginator': None,
33             'page_obj': None,
34             'is_paginated': False,
35         }, context_processors)
36         if not allow_empty and len(queryset) == 0:
37             raise Http404
38     for key, value in extra_context.items():
39         if callable(value):
40             c[key] = value()
41         else:
42             c[key] = value
43     if not template_name:
44         model = queryset.model
45         template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower())
46     t = template_loader.get_template(template_name)
47     return HttpResponse(t.render(c), mimetype=mimetype)

Talk about how this view is incomplete: different Paginator, Jinja2 templates, GET field for page.

 1 def twitter_login_done(request):
 2     request_token = request.session.get('request_token', None)
 3     verifier = request.GET.get('oauth_verifier', None)
 4     denied = request.GET.get('denied', None)
 5     # If we've been denied, put them back to the signin page
 6     # They probably meant to sign in with facebook >:D
 7     if denied:
 8         return HttpResponseRedirect(reverse("socialauth_login_page"))
 9     # If there is no request_token for session,
10     # Means we didn't redirect user to twitter
11     if not request_token:
12         # Redirect the user to the login page,
13         return HttpResponseRedirect(reverse("socialauth_login_page"))
14     token = oauth.Token.from_string(request_token)
15     # If the token from session and token from twitter does not match
16     # means something bad happened to tokens
17     if token.key != request.GET.get('oauth_token', 'no-token'):
18         del_dict_key(request.session, 'request_token')
19         # Redirect the user to the login page
20         return HttpResponseRedirect(reverse("socialauth_login_page"))
21     try:
22         twitter = oauthtwitter.TwitterOAuthClient(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET)
23         access_token = twitter.fetch_access_token(token, verifier)
24         request.session['access_token'] = access_token.to_string()
25         user = authenticate(twitter_access_token=access_token)
26     except:
27         user = None
28     # if user is authenticated then login user
29     if user:
30         login(request, user)
31     else:
32         # We were not able to authenticate user
33         # Redirect to login page
34         del_dict_key(request.session, 'access_token')
35         del_dict_key(request.session, 'request_token')
36         return HttpResponseRedirect(reverse('socialauth_login_page'))
37     # authentication was successful, use is now logged in
38     next = request.session.get('twitter_login_next', None)
39     if next:
40         del_dict_key(request.session, 'twitter_login_next')
41         return HttpResponseRedirect(next)
42     else:
43         return HttpResponseRedirect(LOGIN_REDIRECT_URL)

Pony Magic

view (n.):
Function Callable that takes a Request together with arguments from URL resolver and returns a Response.

In Python, any object with a __call__ attribute is a Callable, e.g:

  • a function
  • a class
  • instance of a class with __call__ method

The View class aka Bikeshed

  • There are many ways to implement the base class. See the Wiki.

  • They all have pros and cons, but most differences are purely cosmetic. This made it the major topic for bikeshedding on django–developers (~200 posts last year).

Finally, a concesus was reached:

 1 class View(object):
 2 
 3     @classmethod
 4     def as_view(cls, **initargs):
 5         def real_view(*args, **kwargs):
 6             instance = cls(**initargs)
 7             return cls.dispatch(*args, **kwargs)
 8         return real_view
 9 
10 # urls.py
11 url('^sample-view/$', View.as_view(), name="sample_view")

A simple example

 1 class ArticleList(View):
 2     template_name = "article_list.html"
 3 
 4    def get(self, request, *args, **kwargs):
 5         self.article_list = self.get_queryset()
 6         return self.render_to_response(
 7             self.get_context_data(articles=self.article_list))
 8 
 9     def render_to_response(self, context):
10         return render_to_response(self.get_template_names(),
11             context, RequestContext(self.request))
12 
13     def get_queryset(self):
14         return Article.objects.all()
15 
16     def get_template_names(self):
17         return [self.template_name] if self.template_name is not None else []
18 
19     def get_context_data(self, **kwargs):
20         return kwargs
21 
22 # urls.py
23 url('^articles/$', ArticleList.as_view(template_name="alternative.html"))

Using mixins

You can compose mixins to reuse common functionality:

 1 from django.views.generic.base import TemplateResponseMixin, View
 2 from django.views.generic.list import ListView, MultipleObjectMixin
 3 
 4 class ArticleList(TemplateResponseMixin, MultipleObjectMixin, View):
 5     model = Article 
 6     template_name = "articles/article_list.html"
 7 
 8     def get(self, request, *args, **kwargs):
 9         self.object_list = self.get_queryset()
10         context = self.get_context_data(object_list=self.object_list)
11         return self.render_to_response(context)
12 
13 class ArticleListTwo(ListView):
14     model = Article

You can also use mixins to override functionality:

1 class JSONArticles(JSONMixin, ArticleList):
2     pass

More generic views

All function-based views have been migrated to CBVs.

  • direct_to_templateTemplateView
  • redirect_toRedirectView
  • object_listListView
  • object_detailDetailView
  • create_objectCreateView
  • archive_yearYearArchiveView
  • etc.

Together with some useful mixins, e.g:

  • FormMixin - form validation methods
  • ModelFormMixin - extends FormMixin to work with ModelForms.

Simple form post processing

 1 from django.views.generic import CreateView
 2 from myapp.signals import custom_signal
 3 
 4 class CreateArticleView(CreateView):
 5 
 6     def get_form_kwargs(self):
 7         kwargs = super(CreateArticleView, self).get_form_kwargs()
 8         # by default this will be 'data', 'files' and 'initial'
 9         kwargs['author'] = self.request.user
10         return kwargs
11 
12     def form_valid(self, form):
13         """Called when the form is valid"""
14         response = super(CreateArticleView, self).form_valid(form)
15         if self.object:
16             custom_signal.send(sender=type(self.object), instance=self.object)
17         # .. or add a Celery task, a message, etc.
18         return response

Overriding tricks

 1 class DelayedModelFormMixin(ModelFormMixin):
 2 
 3     def form_valid(self, form):
 4         self.object = form.save(commit=False)
 5         self.prepare_object_for_save(self.object)
 6         self.object.save()
 7         self.object.save_m2m()
 8         # emit the signal here...
 9         return super(ModelFormMixin, self).form_valid(form)
10 
11     def prepare_object_for_save(self, obj):
12         pass
13 
14 class CreateCommentView(DelayedModelFormMixin, CreateView):
15 
16     def prepare_object_for_save(self, obj)
17         obj.author = self.request.user

Decorating

In URLconf

Because the as_view() method produces an ordinary function, you can apply function decorators to the result:


1 from django.views.generic import TemplateView
2 from django.contrib.auth.decorators import login_required
3 
4 url(r'^/protected/$', login_required(
5         TemplateView.as_view(template_name="secret.html")))

This is simple, but after decorating you're left with a function, so subclassing is not possible.

Decorating methods

Django provides an easy way to create method decorators:


1 from django.utils.decorators import method_decorator
2 
3 class ProtectedView(View):
4 
5     @method_decorator(login_required):
6     def dispatch(self, request, *args, **kwargs):
7         return super(ProtectedView, self).dispatch(request, *args, **kwargs)

Note: Any attributes on dispatch() will get copied to the function returned by as_view(), so that csrf_exempt decorator works correctly.

You can also decorate get(), post(), put() and delete() this way, but csrf_exempt won't work with those methods.

Class decorators

This actually didn't make it to Django 1.3, but I think it's very useful (requires Python 2.6):

 1 def view_decorator(fdec)
 2     def decorator(cls):
 3         original = cls.as_view.im_func
 4 
 5         @functools.wraps(original)
 6         def as_view(current, **initkwargs):
 7             return fdec(original(current, **initkwargs))
 8 
 9         cls.as_view = classmethod(as_view) 
10         return cls
11     return decorator
12 
13 # usage:
14 
15 @view_decorator(login_required)
16 class ProtectedView(TemplateView):
17     pass
18 
19 # but **DO NOT** do this:
20 
21 ProtectedView = view_decorator(login_required)(TemplateView)

Decorators vs. Mixins

  • Both provide a way to add functionality in a reusable way.
  • Decorators can't be overriden or extended, so you shouldn't use them in generic views.
  • Mixins are not a very widely used concept in Python world and requires knowledge about MRO (Method Resolution Order).
  • Neither are class decorators (Python 2.6+)
  • Mixins don't work with function-based views. If you're writing a reusable application, you want to provide a decorator too.

There are no strict rules when to use one over other.

Thank you !

Attributions

Questions ?