django snakebite: when things get evaluated

I just got bit by a small oversight on my part that seemed elusive and destructive enough to post here. It’s easier to show than explain, so here’s the code in question:

# links/urls.py

from django.conf.urls.defaults import *
from django.views.generic import list_detail

from links.models import LinkCategory, Link

urlpatterns = patterns(
    ,
    (r‘^$’,
     list_detail.object_list,
     { ‘queryset’: LinkCategory.objects.all(),
       ‘template_object_name’: ‘category’,
       ‘extra_context’: {‘uncategorized’: Link.published_objects.filter(category__isnull=True)}
      }, ‘links-linkcategory-list’
    ),
)

This uses django’s list_detail generic view to list links sorted by category (the template uses the LinkCategory.link_set manager to render each category’s links). To catch uncategorized links I got clever and brought another queryset into the template context that finds all links with an empty ‘category’ field.

This worked just fine in development, when I was trying my project out using Django’s development server. When I ran it on my server under mod_python however, the ‘uncategorized’ list wouldn’t update! I could add new uncategorized links in the admin but they wouldn’t show up in the view until I restarted apache.

It’s probably pretty clear what the problem was, but it wasn’t clear to me at first (and that’s why I’m writing it up). My clever call to Link.objects.filter(category__isnull=True) only gets evaluated once, the first time the URLconf is loaded. The development server is reloading itself pretty regularly (unless you run with the –noreload option), which is why I never saw this behavior in development; but apache only reloads the module when it gets restarted.

My solution was to write a cron job that restarts apache every 15 seconds.

Just kidding!

The clean solution is to get this logic out of my URLconf and put it somewhere more natural and re-usable (like a template tag). But I’m feeling lazy so here’s a lazy solution:

‘extra_context’: {‘uncategorized’: lambda: Link.objects.filter(category__isnull=True)}

Just enclose the activity in an anonymous function and it gets evaluated anew each time the template is rendered.

If it seems a little unfair that this should work:

(r‘^$’, list_detail.object_list, { ‘queryset’: LinkCategory.objects.all() })

But that’s due to some work in the definition of list_detail.object_list (and all the other generic views that come with Django):

def object_list(request, queryset, a bunch of other arguments):
    # eventually, on line 46 or so…
    queryset = queryset._clone()

That queryset._clone() method does just what it says - and now we have a fresh, uncached queryset to be evaluated and rendered into the template.


About this entry