JSON, Object Oriented Views, and Starting a Real App
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 dict
s 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 dict
s, 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)
<span class="k">def</span> <span class="nf">compute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">op</span><span class="p">):</span>
<span class="n">a</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">[</span><span class="s">'a'</span><span class="p">])</span>
<span class="n">b</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">[</span><span class="s">'b'</span><span class="p">])</span>
<span class="k">return</span> <span class="n">JsonResponse</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span>
<span class="n">op</span><span class="o">=</span><span class="n">op</span><span class="p">,</span> <span class="n">a</span><span class="o">=</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">=</span><span class="n">b</span><span class="p">,</span>
<span class="n">result</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">perform_operation</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)))</span>
<span class="k">def</span> <span class="nf">url_patterns</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="p">[</span><span class="n">url</span><span class="p">(</span><span class="n">regex</span><span class="o">=</span><span class="s">r'^compute/(?P<op>\w+)/$'</span><span class="p">,</span> <span class="n">view</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">compute</span><span class="p">)]</span>
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
<span class="k">def</span> <span class="nf">constant</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">const</span><span class="p">):</span>
<span class="k">return</span> <span class="n">JsonResponse</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">const</span><span class="p">,</span> <span class="n">val</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_constant</span><span class="p">(</span><span class="n">const</span><span class="p">)))</span>
<span class="k">def</span> <span class="nf">url_patterns</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">ExtendedMathAPI</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">url_patterns</span><span class="p">()</span> <span class="o">+</span> <span class="p">[</span>
<span class="n">url</span><span class="p">(</span><span class="s">r'^const/(?P<const>\w+)/'</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">constant</span><span class="p">)]</span>
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()
<span class="k">def</span> <span class="nf">detail</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">obj</span><span class="o">.</span><span class="n">as_dict</span><span class="p">()</span>
<span class="k">if</span> <span class="nb">bool</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s">'html'</span><span class="p">,</span> <span class="bp">False</span><span class="p">)):</span>
<span class="n">d</span><span class="p">[</span><span class="s">'html'</span><span class="p">]</span> <span class="o">=</span> <span class="n">obj</span><span class="o">.</span><span class="n">passage</span><span class="o">.</span><span class="n">html</span>
<span class="k">return</span> <span class="n">d</span>
<span class="k">def</span> <span class="nf">list_entry</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
<span class="k">return</span> <span class="n">obj</span><span class="o">.</span><span class="n">as_dict</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">list_view_POST</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="n">passage</span> <span class="o">=</span> <span class="n">MemoryPassage</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create_from_ref</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">POST</span><span class="p">[</span><span class="s">'ref'</span><span class="p">])</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">passage</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">next_view</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="n">to_review</span> <span class="o">=</span> <span class="n">MemoryPassage</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s">'last_reviewed'</span><span class="p">)[</span><span class="mf">0</span><span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">detail</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">to_review</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">url_patterns</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s">''</span><span class="p">):</span>
<span class="n">url_prefix</span> <span class="o">=</span> <span class="n">prefix</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">url</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">MemoryPassageResource</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">url_patterns</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span> <span class="o">+</span> \
<span class="p">[</span><span class="n">url</span><span class="p">(</span><span class="s">'^</span><span class="si">%s</span><span class="s">/next/$'</span> <span class="o">%</span> <span class="n">url_prefix</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">next_view</span><span class="p">)]</span>
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.