JSON, Object Oriented Views, and Starting a Real App

June 22, 2008. Filed under django 72 python 56

Welcome to the third installment of Wielding Django, kindly hosted by Will Larson. As I promised in the first installment, this is where people who've worked with Django before can tune in. We'll touch on JSON and object-oriented views before starting on a real (if tiny) app.

We'll make use of some code from django-webapp, which is a collection of code and design patterns that I've found useful for webapp development. I extracted some of them as snippets, but you'll probably find it most convenient to just grab the code:

svn checkout http://django-webapp.googlecode.com/svn/trunk/ django-webapp

JSON

Last time we just returned a single raw number, but it's easy to return more structured data. We could use XML, but it's a heavy and slow (in some implementations) encoding. The unofficial standard for fast, lightweight encoding is JSON, and it's really easy to generate in Python. Here's the compute function from last time, returning JSON instead.

from dwa.serialize import JsonResponse

def compute(request, op):
    import operator
    operation = getattr(operator, op)
    a = float(request.GET['a'])
    b = float(request.GET['b'])
    result = operation(a, b)
    return JsonResponse(dict(
             op=op, a=a, b=b,
            result=operation(a, b)))

I prefer the dict(abc=de) notation for most dicts with more than one item. It avoids having to quote the keys, and it's used for keyword arguments in general, which I generally really like. But it's identical to {'op': op, 'a': a}, etc.

dwa.serialize is part of django-webapp and is also snippet 801 (which needs snippet 800). And JsonResponse just does the JSON encoding and wraps the result in an HttpResponse. (By default, it surrounds the result with JavaScript comments for security; most JavaScript frameworks can deal with that.) Note that the top-level object must be a dict (not even an array) for JSON.

But there's an even easier way. Django supports "middleware", which just provides a set of hooks into various parts of the request and response process. We can intercept the response processing and JSON-encode it if necessary. That's exactly what snippet 803 does. (That's in django-webapp too.) To enable the middleware, add this to settings.py:

MIDDLEWARE_CLASSES = (
    'dwa.middleware.JsonEncodingMiddleware', # Keep this last!
    )

Then your views can just return dicts, and they'll get encoded as JSON.

Ajax error middleware

If your server hits an error while handling an AJAX request, even the trusty Firebug makes the actual error hard to see. So you might want to add AjaxErrorMiddleware, which fixes that. My MIDDLEWARE_CLASSES looks like:

MIDDLEWARE_CLASSES = (
    'dwa.middleware.AjaxErrorMiddleware',
    'dwa.middleware.JsonEncodingMiddleware',
    )

Just remember to keep JsonEncodingMiddleware last so that it gets first dibs on what your views return.

Object-Oriented Views

A Django view is simply a callable that gets an HttpRequest as the first parameter. What sorts of things qualify?

1. A function:

def my_view(request, **kwargs)

2. A class:

class MyView(YourView):
    def __call__(self, request, **kwargs):
         return super(MyView, self)(request, **kwargs)

3. An instance method:

class MyResource(Resource):
    def list(self, request):
        return {'contents': list(self.queryset)}

Perhaps there are more that I have not thought of. Anyway, Django tutorials -- including this one -- usually start with view functions. Sometimes view classes are presented as a more advanced technique. I've yet to see instance methods used anywhere, but I've found them to be very useful, so I'm giving them airtime here.

For a trivial example, let's rewrite our little math app to use object-oriented views:

from django.conf.urls.defaults import url, handler404, handler500
from dwa.serialize import JsonResponse

class MathAPI(object):
    def perform_operation(self, op, a, b):
        import operator
        operation = getattr(operator, op)
        return operation(a, b)

    def compute(self, request, op):
        a = float(request.GET['a'])
        b = float(request.GET['b'])
        return JsonResponse(dict(
                op=op, a=a, b=b,
                result=self.perform_operation(op, a, b)))

    def url_patterns(self):
        return [url(regex=r'^compute/(?P<op>\w+)/$', view=self.compute)]


urlpatterns = MathAPI().url_patterns()
urlpatterns.append(
    url(r'^$', 'django.views.static.serve', dict(document_root='public', path='index.html')))

A few observations:

1. We cleanly separated all the URL patterns that go with this API. For a general solution, we would probably want to add a prefix parameter to permit easily changing where the API is hosted. We could even subclass the Django internal URL resolver, but that's not really designed to be extended easily.

2. The actual computation (perform_operation) is separated from the web interface to it (compute), so you could subclass either one independently. For a silly example:

class HtamAPI(MathAPI):
    def perform_operation(self, op, a, b):
        return super(HtamAPI, self).perform_operation(op, a, b)

urlpatterns = HtamAPI().url_patterns()

3. It's easy to add new functions:

class ExtendedMathAPI(MathAPI):
    def get_constant(self, const):
        if const == 'pi': return 3.1
        elif const == 'e': return 2.7

    def constant(self, request, const):
        return JsonResponse(dict(name=const, val=self.get_constant(const)))

    def url_patterns(self):
        return super(ExtendedMathAPI, self).url_patterns() + [
            url(r'^const/(?P<const>\w+)/', self.constant)]

A Real Webapp

The rest of this series will be devoted to fleshing out these ideas into useful techniques for fast application development. But to keep me honest, we should do something harder and more interesting than simple math. And it couldn't be another blog, wiki, or reddit clone...

So I decided to do a minimal version of a webapp that I've wanted to build for a while. I love God's Word because it shows me who he is (and he's really good!) and who I am. And I often informally memorize parts of it because it's useful to recall in many situations. But I can often forget some words, or where a passage is, so I wanted something to help me review. It needs to be a tool I can use anywhere, so the best realization of that tool would be punched cards. Okay... how about HTML, CSS, JavaScript, DOM, DHTML, JSON, AJAX, Django, webapp buzzword soup.

The very mention of God gets many people riled up, regardless of viewpoint. If you do feel that you have something to say, (a) think before you post, and (b) post on my blog, so here on Will's blog, focus remains on Django.

I'll go into detail in the next articles, but I'll motivate it by showing my entire server-side interface code (sans database models) below:

from django.conf.urls.defaults import url, handler404, handler500
from dwa.resource import Resource, ResourceCollection
from dwa.api import AjaxAPI
from dwa.util import static_url
from scripmem.models import MemoryPassage, ActivityLogEntry

class MemoryPassageResource(Resource):
    url = 'passages'
    queryset = MemoryPassage.objects.all()

    def detail(self, request, obj):
        d = obj.as_dict()
        if bool(request.GET.get('html', False)):
            d['html'] = obj.passage.html
        return d

    def list_entry(self, obj):
        return obj.as_dict()

    def list_view_POST(self, request):
        passage = MemoryPassage.objects.create_from_ref(request.POST['ref'])
        return self.detail(request, passage)

    def next_view(self, request):
        to_review = MemoryPassage.objects.order_by('last_reviewed')[0]
        return self.detail(request, to_review)

    def url_patterns(self, prefix=''):
        url_prefix = prefix + self.url
        return super(MemoryPassageResource, self).url_patterns(prefix) + \
            [url('^%s/next/$' % url_prefix, self.next_view)]


class ActivityLogResource(Resource):
    url = 'log'
    queryset = ActivityLogEntry.objects.all()

class ScripMemAPI(AjaxAPI):
    @AjaxAPI.exported
    def completed(self, request, passage_id, score):
        passage = MemoryPassage.objects.get(id=passage_id)
        ActivityLogEntry.objects.create(passage=passage, score=int(score))
        return {'status': 'ok'}

scripmem_collection = ResourceCollection((
        MemoryPassageResource,
        ActivityLogResource
        ))

urlpatterns = scripmem_collection.url_patterns() + ScripMemAPI().url_patterns()

# Static serving
static_pages = (
    'index.html',
    'scripmem.js',
    'style.css',
    )

urlpatterns += [static_url(page, page) for page in static_pages]
urlpatterns.append(static_url('', 'index.html'))

Now that's not complete (lacking user management, for one!), but it shows the basic idea. And I think that's a generally useful design strategy: find and implement the core essentials of your idea, then play with it and refine it.

Next time, we'll have a look at some of the techniques that I used in django-webapp to enable my app and yours.

If anyone is genuinely interested in collaborating on this particular app, please contact me directly; it could certainly use a hand.