23 October 2011

A Decorator for Django Ajax Views

I find myself writing a decent number of Django views that are purely for AJAX calls. These AJAX views all have a very common format:

  1. make sure the request is a POST and done via AJAX (error response if fail)
  2. validate the POST with a Django form (error response if fail)
  3. do something with the cleaned data

I got sick of the repetitive code and decided to call in a decorator to solve my problems (more on decorators here). I wrote ajax_view and it works like this:

def ws_add_result(request):
    cl = request.form.cleaned_data
    name = cl['name']
    # and so on…

The content of your view will not get called unless the request and form passed all requirements and validated, and you can access the validated form as an attribute of the request object.

It takes several keyword arguments, which are all optional:

  • FormClass=None — your custom form for this view
  • method='POST' — the type of HTTP method can be ("GET", "POST", or "REQUEST")
  • login_required=True — whether the user has to be logged in or not
  • ajax_required=True — whether the request has to be ajax or not (useful to toggle while testing)
  • json_form_errors=False — whether form errors should be returned in a json dict or as an error response

and here’s the code:

from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseForbidden, HttpResponseBadRequest
from django.utils.safestring import mark_safe
from django.utils import simplejson
from functools import wraps

_ERROR_MSG = '<!DOCTYPE html><html lang="en"><body><h1>%s</h1><p>%%s</p></body></html>'
_400_ERROR = _ERROR_MSG % '400 Bad Request'
_403_ERROR = _ERROR_MSG % '403 Forbidden'
_405_ERROR = _ERROR_MSG % '405 Not Allowed'

def ajax_view(FormClass=None, method='POST', login_required=True, ajax_required=True, json_form_errors=False):
    def decorator(view_func):
        def _ajax_view(request, *args, **kwargs):
            if request.method != method and method != 'REQUEST':
                return HttpResponseNotAllowed(mark_safe(_405_ERROR % ('Request must be a %s.' % method)))

            if ajax_required and not request.is_ajax():
                return HttpResponseForbidden(mark_safe(_403_ERROR % 'Request must be set via AJAX.'))

            if login_required and not request.user.is_authenticated():
                return HttpResponseForbidden(mark_safe(_403_ERROR % 'User must be authenticated!'))

            if FormClass:
                f = FormClass(getattr(request, method))
                if not f.is_valid():
                    if json_form_errors:
                        errors = dict((k, [unicode(x) for x in v]) for k,v in f.errors.items())
                        return HttpResponse(simplejson.dumps({'error': 'form', 'errors': errors}), 'application/json')
                        return HttpResponseBadRequest(mark_safe(_400_ERROR % ('Invalid form<br />' + f.errors.as_ul())))
                request.form = f

            return view_func(request, *args, **kwargs)
        return wraps(view_func)(_ajax_view)
    return decorator

Let me know if this is useful or if you have different conventions for dealing with this situation in Django. Also, I’ve been building a lot of stuff without using the ORM, so maybe there’s a more elegant way of doing something like this with the forms from models or another method, but this has been pretty useful for me.

Comments (7)

1. Jared wrote:
<p> Very useful, What about using GET method instead of POST ? </p>

Posted on 30 October 2011 at 8:10 AM  |  permalink

2. peter wrote:
<p> The function optionally takes all those other arguments too. So you can specify any or none of them, e.g., </p> <pre><code>@ajax_view(MyForm, &#39;GET&#39;) </code></pre> <p> or here’s another example that changes all the defaults: </p> <pre><code>@ajax_view(MyForm, method=&#39;GET&#39;, login_required=False, ajax_required=False, json_form_errors=True) </code></pre>

Posted on 30 October 2011 at 8:10 PM  |  permalink

3. Patrick wrote:
<p> thanks for this. </p>

Posted on 10 December 2011 at 11:12 AM  |  permalink

4. JeungWonKim wrote:
<p> Hi! Thanks for the cool decorator. </p> <p> I&#39;ve tested yours and got an error with wraps function if I try to use decorator without providing any arguments. </p> <p> &quot; &#39;WSGIRequest&#39; object has no attribute &#39;<strong> name </strong>&#39; &quot; </p> <p> I fixed this by passing available_attrs (from django.util.decorators import available_attrs) </p> <p> so it goes like this def ajax_view(...): def decorator(...): @wraps(view_func, assigned=available_attrs(view_func)) def _ajax_view(...): ... return _ajax_view return decorator </p> <p> I know this is an old post, but just in case anyone steps into same error... </p>

Posted on 22 May 2012 at 3:05 AM  |  permalink

5. JeungWonKim wrote:
<p> Oops, </p> <p> from django.utils.decorators import available_attrs def ajax_view(...): ....def decorator(...): [email protected](view_func, assigned=available_attrs(view_func)) ........def _ajax_view(...): ... .........return _ajax_view ....return decorator </p>

Posted on 22 May 2012 at 3:05 AM  |  permalink

6. JeungWonKim wrote:
<p> Sorry for spamming your blog... I just posted django snippet about this decorator. Please let me know if you want this post to be deleted. </p> <p> http://djangosnippets.org/snippets/2755/ </p>

Posted on 22 May 2012 at 4:05 AM  |  permalink

7. ouhouhsami wrote:
<p> For Ajax calls, I often use Dajax and Dajaxice (http://www.dajaxproject.com/), which are sometimes a bit tricky to configure, but which work great, and simplify all ajax stuff. May be it could help you solve the same kind of problem you found. </p>

Posted on 22 May 2012 at 4:05 AM  |  permalink

Did you find this helpful or fun? Please donate!

donate via btc or eth

btc: 18jCzwsZDGQYcs6Kyv92pd4683cnnxm1Dd
eth: 0xC285F21Cb271Cb4B3F70c4C47B2f7B26063AF590
paypal: paypal.me/mrcoles

Peter Coles

Peter Coles

is a software engineer who lives in NYC, works at Ringly, and blogs here.
More about Peter »

github · soundcloud · @lethys · rss

It’s time to get big money out of politics. Join the kick-started campaign to put government back in the hands of the people. Pledge mayday.us now