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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
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 :)
#
@gsakkis: good point, thanks!
#
I updated the code, also now I use
__new__
instead of__init__
in the metaclass, and use thedct
argument to add members to the class instead of assigning tocls
.I also added a
before
method which I personally use a lot, but initially left out of this snippet.Comments and critique are welcome!
#
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.