Login

Convenient class based views

Author:
eallik
Posted:
May 27, 2010
Language:
Python
Version:
1.2
Score:
3 (after 3 ratings)

I needed to use class based views, but I wanted to be able to use the full name of the class in my URLconf without always having to instantiate the view class before using it. What helped me was a surprisingly simple metaclass.

I can now both instantiate view classes and use the instances as view functions, OR I can simply point my URLconf to my class and have the metaclass instantiate (and call) the view class for me. This works by checking the first argument to __call__ – if it's a HttpRequest, it must be an actual HTTP request because it would be nonsense to attept to instantiate a view class with an HttpRequest instance.

The View base class contains a overridable before method that can be used to add a common procedure to handlers of different HTTP requests. The before method can modify the args and kwargs of the request handlers to be able to replace, say, model_id with model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from django.http import HttpResponse, HttpRequest, HttpResponseNotAllowed


class CallableClass(type):
    def __new__(cls, name, bases, dct):
        if 'HEAD' not in dct and 'GET' in dct:
            # XXX: this function could possibly be moved out
            # to the global namespace to save memory.
            def HEAD(self, request, *args, **kwargs):
                response = self.GET(request, *args, **kwargs)
                response.content = u''
                return response
            dct['HEAD'] = HEAD

        dct['permitted_methods'] = []
        for method in ('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE'):
            if hasattr(dct.get(method, None), '__call__'):
                dct['permitted_methods'].append(method)

        return type.__new__(cls, name, bases, dct)

    def __call__(cls, *args, **kwargs):
        if args and isinstance(args[0], HttpRequest):
            instance = super(CallableClass, cls).__call__()
            return instance.__call__(*args, **kwargs)
        else:
            instance = super(CallableClass, cls).__call__(*args, **kwargs)
            return instance


class View(object):
    __metaclass__ = CallableViewClass

    def __call__(self, request, *args, **kwargs):
        if request.method in self.permitted_methods:
            handler = getattr(self, request.method)
            # XXX: Could possibly check if 'before' returns a response
            # and return that instead.
            self.before(request, args, kwargs)
            return handler(request, *args, **kwargs)

        return HttpResponseNotAllowed(self.permitted_methods)

    def before(self, request, args, kwargs):
        """Override this method to add common functionality to all HTTP method handlers.

        args and kwargs are passed as regular arguments so you can add/remove arguments:
            def before(self, request, args, kwargs):
                kwargs['article'] = get_object_or_404(Article, id=kwargs.pop('article_id')
            def GET(self, request, article): # <== 'article' instead of 'article_id'
                ...
            def POST(delf, request, article): # <== 'article' instead of 'article_id'
                ...
        """
        pass


class MyView(View):
    def __init__(self, arg=None):
        self.arg = arg
    def GET(request):
        return HttpResponse(self.arg or 'No args passed')

@login_required
class MyOtherView(View):
    def POST(request):
        return HttpResponse()

# in urls.py
# And all the following work as expected.
urlpatterns = patterns(''
    url(r'^myview1$', 'myapp.views.MyView', name='myview1'),
    url(r'^myview2$', myapp.views.MyView, name='myview2'),
    url(r'^myview3$', myapp.views.MyView('foobar'), name='myview3'),
    url(r'^myotherview$', 'myapp.views.MyOtherView', name='otherview'),
)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 2 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 2 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 9 months, 1 week ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 9 months, 4 weeks ago
  5. Help text hyperlinks by sa2812 10 months, 3 weeks ago

Comments

gsakkis (on May 27, 2010):

Why not compute the permitted_methods in the metaclass and store the result instead of recomputing them every time ? Hopefully you don't add dynamically methods to your views :)

#

eallik (on May 31, 2010):

@gsakkis: good point, thanks!

#

eallik (on May 31, 2010):

I updated the code, also now I use __new__ instead of __init__ in the metaclass, and use the dct argument to add members to the class instead of assigning to cls.

I also added a before method which I personally use a lot, but initially left out of this snippet.

Comments and critique are welcome!

#

eallik (on June 8, 2010):

Actually there might be an issue with using instantiated views, so might just assume the class will never be instantiated.

#

Please login first before commenting.