Class based views in Django

Abstract

Never been frustrated doing functional programming in your django views ? This little module is intended to help you use objects for your views :

  • Replace urls parameters and template context by class attributes.
  • Use inheritance to group common task, do specializations, common decorators...
  • Define template tags in your objects, call instance methods.
  • Define your re-usable library.

This code is now quite complete and we use it in production environment.

Hello World comparison

Consider this example (classic way):

urls.py

from django.conf.urls.defaults import *
from views import hello

urlpatterns = patterns(
   '',
   (r'^hello/(?P<times>\d+)/$', hello),
)

views.py

from django.shortcuts import render_to_response
from django.template import RequestContext

def hello(request, times=10):
    name = request.user and request.user.username or 'Pony'
    times = range(int(times))
    return render_to_response('hello.html',
                              { 'times' : times, 'name' : name },
                              context_instance=RequestContext(request))

hello.html

{% for i in times %}
Hello {{ name }}
{% endfor %}

Now with ViewsObject:

urls.py

from django.conf.urls.defaults import *
from django_pimentech.viewsobjects import ObjectCaller
from views import Hello

urlpatterns = patterns(
   '',
   (r'^hello/(?P<times>\d+)/$', ObjectCaller(Hello)),
)

views.py

from django_pimentech.viewsobjects import BaseView

class Hello(BaseView):
     template = 'hello.html'
     name = None
     times = 10

     def fill_context(self):
         self.times = range(int(self.times))
         self.name = self.request.user and self.request.user.username or 'Pony'

Besides the object syntax, the main idea of this system is that url parameters and context are regrouped in attributes and method classes : each public attribute or method will be accessible by the template.

That's it. We have implemented all the views function capabilities, as you will see below.

Usage

URLs

from django.conf.urls.defaults import *
from django_pimentech.viewsobjects import ObjectCaller
from views import MyView

urlpatterns = patterns(
   '',
   (r'^test/(?P<param1>\w+)/(?P<param2>\w+)/(?P<paramn>\w+)/$', ObjectCaller(MyView)),
)
  • Each parameter will be a MyView instance attribute.
  • The ObjectCaller class is important here : when you import the urls module, the ObjectCaller is initialized (__init__ method). Then, each time the url above is called, the ObjectCaller instance is called, like a function (__call__ method). If you put "MyView" directly on urls module, the same instance will be shared by multiple http clients, be warned !

View definition and context

from django_pimentech.viewsobjects import BaseView

class MyView(BaseView):
   template = 'my_template.html'

   param1 = default_value
       param2 = None
   ...
       paramn = None

   def fill_context(self):
       self.param2 = self.something()
  • Define the template attribute if you want to render a template. You can also overload the get_template method (see source).
  • Each attribute defined in your class (here param1..n) defined from the first BaseView inheritance will be integrated in the request context [1] .
  • you can access to the request variable with self.request
[1]The request context parameters list is defined in module import stage (see meta-class in source), for better performance.

Inheritance

Sure ! In most cases, it is better to overload fill_context method, and reserve __call__ method overload for decorators and authentication related operations.

Decorators

You have to decorate the __call__ BaseView method, decorated with the special "on_method" decorator

@on_method(login_required)
def __call__(self, request, *args, **kwargs):
    self.authenticated_user = request.user
    self.employe = self.authenticated_user.get_profile()
    if kwargs.has_key('note'):
        kwargs['note'] = self.get_note_with_perms(kwargs['note'])

    return super(ActiveMemberRequiredView, self).__call__(request, *args, **kwargs)

You can also use the "decorate_method" function like this

decorate_method(MyView.__call__, login_required)

Template tags and filters as methods

The instance itself is also passed in request context, as self attribute. With self attribute, you can call any public method that doesn't require parameters

{{ self.my_method }}

If your method has parameters, you can use the call template filter automaticaly defined when you import viewsobjects

{% call "toto" "popo" forloop.counter %}

will call the toto method of your viewsobject instance with the parameters "popo" and forloop.counter.

You can also easily define template tags like this

def module_note(self, note):
    return self.inclusion_tag("note.html", { 'new': False,
                                             'note': note})

called in your template with

{% call "module_note" myvar %}

Note that the template tag is only compiled once at it's first use.

HttpResponse

In your fill_context method, you can return a HttpResponse (a 404 for example), the "render_to_response" mechanism will be automaticaly shorted.

JSON response

A little shortcut is provided if you have to render JSON instead of a template : simply overload the render method like this

def render(self):
    return self.render_json()

With this configuration, all your context will be returned in a JSON HttpResponse string. If you want to return only some variables, pass your structure to the render_json method

def render(self):
    return self.render_json(self.my_result)

Installation

  • get PimenTech libcommonDjango :
svn checkout http://svn.pimentech.org/pimentech/libcommonDjango
  • install it with "make install" [2]
[2]or python setup.py install in libcommonDjango dir

Trac access : http://trac.pimentech.org/pimentech/browser/libcommonDjango

Source

You can also directly get the source here.

# -*- coding: utf-8 -*-

# !!!! TEST VERSION WITH PROPERTY ATTRS !!!

import sys

from django.conf import settings
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import defaulttags, RequestContext, TemplateSyntaxError, Node, Context
from django.template.loader import get_template, select_template
from UserDict import UserDict
from pdjango_json_encoder import PDjangoJSONEncoder

try:
    from django.utils import simplejson
except ImportError:
    pass # does'nt work in 0.97
import types

class __Foo:
    @property
    def attr(self):
        pass

PropertyType = type(__Foo.attr)

class ObjectCaller:
    def __init__(self, klass, *args, **kwargs):
        self.klass = klass
        self.args = args
        self.kwargs = kwargs
        self.__name__ = klass.__name__

    def __call__(self, *args, **kwargs):
        obj = self.klass(*(self.args), **(self.kwargs))
        return obj(*args, **kwargs)

class ObjectCallerStr:
    def __init__(self, klass_name, *args, **kwargs):
        self.klass_name = klass_name
        self.args = args
        self.kwargs = kwargs
        self.module, self.__name__ = self.klass_name.rsplit('.', 1)

    def __call__(self, *args, **kwargs):
        if self.module not in sys.modules:
            __import__(self.module)
        self.klass = getattr(sys.modules[self.module], self.__name__)
        obj = self.klass(*(self.args), **(self.kwargs))
        return obj(*args, **kwargs)

class ViewContext(UserDict):
    def __init__(self, view, dict=None, **kwargs):
        self.__view = view
        self._lazy_attrs = set()
        UserDict.__init__(self, dict, **kwargs)

    def __getitem__(self, key):
        if key in self._lazy_attrs:
            return getattr(self.__view, key)
        return UserDict.__getitem__(self, key)

    def has_key(self, key):
        return key in self.data or key in self._lazy_attrs

    def __contains__(self, key):
        return key in self.data or key in self._lazy_attrs


class MetaViewClass(type):
    def __new__(meta, classname, bases, classDict):
        if classname != 'BaseView':
            classDict['_context_attrs'] = {}
            classDict['_context_lazy_attrs'] = set()
            for base in bases:
                if base.__dict__.has_key('_context_attrs'):
                    classDict['_context_attrs'].update(base._context_attrs)
                if base.__dict__.has_key('_context_lazy_attrs'):
                    classDict['_context_lazy_attrs'].update(base._context_lazy_attrs)
            for attname, value in classDict.items():
                if attname[0] != '_' and not type(value) in (types.ClassType, types.FunctionType):
                    if type(value) == PropertyType:
                        classDict['_context_lazy_attrs'].add(attname)
                    else:
                        classDict['_context_attrs'][attname] = attname

        return type.__new__(meta, classname, bases, classDict)


class BaseView(object):
    __metaclass__ =  MetaViewClass

    template = ''
    request = None
    context = None

    _JSONEncoder = PDjangoJSONEncoder

    def __init__(self, *args, **kwargs):
        self.context = ViewContext(self)
        self.template = kwargs.get('template') or self.template
        self.context.update(kwargs.get('extra_context', {}))


    def get_template(self):
        return self.template


    def render_html(self):
        context_instance = RequestContext(self.request, self.context)
        return render_to_response(
            self.get_template(),
            None,
            context_instance=context_instance)

    def render_jsonp(self, callback, to_pack=None):
        json = to_pack is None and self.context or to_pack
        if json == self.context:
            del self.context['self']
        content = '%s(%s);' % (callback, simplejson.dumps(json, ensure_ascii=False, cls=self._JSONEncoder))
        return HttpResponse(content, content_type='application/javascript; charset=%s' % settings.DEFAULT_CHARSET)

    def render_json(self, to_pack=None, content_type='application/json'):
        response = HttpResponse(content_type='%s; charset=%s' % (content_type, settings.DEFAULT_CHARSET))
        if to_pack is None:
            del self.context['self']
            json = self.context
        else:
            json = to_pack
        simplejson.dump(json, response, ensure_ascii=False, cls=self._JSONEncoder)
        return response


    def render(self):
        return self.render_html()


    def fill_extra_context(self):
        # DEPRECATED
        pass


    def fill_context(self):
        """Defined in subclasses"""
        pass


    def _fill_context_with_attrs(self):
        for attname in self._context_attrs.keys():
            if not self.context.has_key(attname):
                self.context[attname] = getattr(self, attname)
        for attname in self._context_lazy_attrs:
            self.context._lazy_attrs.add(attname)


    def set_initial_attrs(self, request, kwargs):
        self.request = request
        self.context['self'] = self
        for var, value in kwargs.items():
            setattr(self, var, value)


    def pre_process(self):
        pass


    def process(self):
        response = self.fill_context()
        if hasattr(response, 'status_code'):
            # http response
            return response
        response = self.fill_extra_context()
        if hasattr(response, 'status_code'):
            # http response
            return response
        self._fill_context_with_attrs()
        return self.render()


    def __call__(self, request, *args, **kwargs):
        self.set_initial_attrs(request, kwargs)
        self.pre_process()
        return self.process()


    __compiled_template__ = None
    def inclusion_tag(self, file_name, extra_context=None):
        if self.__compiled_template__ is None:
            self.__compiled_template__ = {}
        nodelist = self.__compiled_template__.get(file_name)
        if nodelist is None:
            if not isinstance(file_name, basestring) and getattr(file_name, '__iter__', False):
                t = select_template(file_name)
            else:
                t = get_template(file_name)
            nodelist = self.__compiled_template__[file_name] = t.nodelist

        if extra_context:
            context = self.context.copy()
            context.update(extra_context)
        else:
            context = self.context
        return nodelist.render(Context(context))


    def redirect_self(self):
        return HttpResponseRedirect(self.request.get_full_path())




# Thanks to Todd Reed from http://www.toddreed.name/content/django-view-class/
def on_method(function_decorator, *deco_args, **deco_kwargs):
    def decorate_method(unbound_method):

        def method_proxy(self, *args, **kwargs):
            def f(*a, **kw):
                return unbound_method(self, *a, **kw)

            return function_decorator(f, *deco_args, **deco_kwargs)(*args, **kwargs)

        return method_proxy

    return decorate_method



# use this function to decorate like this :
# decorate_method(MyView.__call__, cache_page)

def decorate_method(method, decorator, *args, **kwargs):
    setattr(method.im_class, method.im_func.func_name, on_method(decorator, *args, **kwargs)(method))

# inspired by http://code.djangoproject.com/wiki/CallTag
class CallNode(Node):
    def __init__(self, method_name, *args, **kwargs):
        self.method_name = method_name
        self.args = args
        self.kwargs = kwargs

    def render(self, context):
        method_name = self.method_name.resolve(context)
        method = getattr(context['self'], method_name)
        d = {}
        args = d['args'] = []
        args = [ arg.resolve(context) for arg in self.args ]
        return method(*args)

def do_call(parser, token):
    bits = token.split_contents()
    if len(bits) < 2:
        raise TemplateSyntaxError, "%r tag takes at least one argument" % bits[0]
    method_name = parser.compile_filter(bits[1])
    if method_name.token[1] == '_':
        raise TemplateSyntaxError, "Private method calls are not allowed: %r" % method_name.token
    args = [ parser.compile_filter(arg) for arg in bits[2:] ]
    return CallNode(method_name, *args)

defaulttags.register.tag('call', do_call)
# Perhaps the line above is not very academic




Note

We would be pleased to receive your feedbacks, corrections and improvement suggestions.

Commentaires

Jared Forsyth Juillet 8, 2010 at 5:10 après-midi

I really like the looks of this! I'll be interested to see how the final API plays out, though -- the "call" tag seems a bit too hackish...

Jared Forsyth Juillet 8, 2010 at 5:18 après-midi

Two other notes ;)

1) The "ObjectCaller" class looks like overkill -- couldn't you do a django.functional.curry? Or for a more OO look, have:

urlpatterns = patterns(
'',
(r'^hello/(?P<times>\d+)/$', Hello.handler()),
)

or some such function, which would just look like

class BaseView:
handler = classmethod(django.utils.functional.curry)

aaand the second thing:

2) I'd love it if you put this on Github so I could fork it, play around, and push stuff upstream. Github really enables community development...
If you don't want to do that, I'll put a mirror up there just for my own enjoyment ;)

Jared Forsyth Juillet 8, 2010 at 5:22 après-midi

BTW you should trick out your blog w/ DISQUS

http://jaredforsyth.com/blog/2010/may/4/...
http://disqus.com/

Comments

blog comments powered by Disqus