Tag Archives: urlpatterns

Grouping URLs in Django routing

One of the things I liked (and still like) about Django is that request routing is configured with regular expressions. You can capture positional and named parts of the request path, and the request handler will be invoked with the captured strings as positional and/or keyword arguments.

Quite often I find that the URL patterns repeat a lot of the regular expressions with minor variations for different but related view functions. For example, suppose you want CRUD-style URLs for a particular resource, you would write an urls.py looking something like:

from django.conf.urls import url, patterns

urlpatterns = patterns('myapp.views',
    url(r'^(?P<slug>[-\w]+)/$', 'detail'),
    url(r'^(?P<slug>[-\w]+)/edit/$', 'edit'),
    url(r'^(?P<slug>[-\w]+)/delete/$', 'delete'),
)

The detail, edit and delete view functions (defined in myapp.views) all take a slug keyword argument, so one has to repeat that part of the regular expression for each URL.

When you have more complex routing configurations, repeating the (?P<slug>[-\w]+)/ portion of each route can be tedious. Wouldn’t it be nice to declare that a bunch of URL patterns all start with the same capturing pattern and avoid the repetition?

It would be nice.

I want to be able to write an URL pattern that defines a common base pattern that the nested URLs extend:

from django.conf.urls import url, patterns, group
from myapp.views import detail, edit, delete

urlpatterns = patterns('',
    group(r'^(?P<slug>[-\w]+)/',
        url(r'^$', detail),
        url(r'^edit/$', edit),
        url(r'^delete/$', delete),
    ),
)

Of course there is no group function defined in Django’s django.conf.urls module. But if there were, it would function like Django’s include but act on locally declared URLs instead of a separate module’s patterns.

It happens that this is trivial to implement! Here it is:

from django.conf.urls import url, patterns, RegexURLResolver
from myapp.views import detail, edit, delete

def group(regex, *args):
    return RegexURLResolver(regex, args)

urlpatterns = patterns('',
    group(r'^(?P<slug>[-\w]+)/',
        url(r'^$', detail),
        url(r'^edit/$', edit),
        url(r'^delete/$', delete),
    ),
)

This way the detail, edit and delete view functions still get invoked with a slug keyword argument, but you don’t have to repeat the common part of the regular expression for every route.

There is a problem: it won’t work if you want to use a module prefix string (the first argument to patterns(...)). You either have to give a full module string, or use the view objects directly. So you can’t do this:

urlpatterns = patterns('myapp.views',
    # Doesn't work.
    group(r'^(?P<slug>[-\w]+)/',
        url(r'^$', 'detail'),
    ),
)

Personally I don’t think this is much of an issue since I prefer to use the view objects, and if you are using class-based views you will likely be using the view objects anyway.

I don’t know if “group” is a good name for this helper function. Other possibilities: “prefix”, “local”, “prepend”, “buxtonize”. You decide.

reverse() chicken and egg problem

I wound up in a chicken and egg situation today using Django’s syndication framework and the reverse helper. The problem was that immediately after starting the development server, Django would throw a NoReverseMatch exception on the first client visit, followed by AttributeError on all subsequent visits.

It all started so innocently… I had wanted a set of urls for my application like this:

So I put the following in the application’s urls.py:

# myapp/urls.py
from django.conf.urls.defaults import *
from views import arrivals_list, departures_list
from feeds import LatestArrivals, LatestDepartures


feed_dict = {'a': LatestArrivals, 'd': LatestDepartures}


urlpatterns = patterns('',
    (r'^a/$', arrivals_list, {}, 'arrivals'),
    (r'^d/$', departures_list, {}, 'departures'),
    (r'^(?P<url>[ad])/feed/$', 'django.contrib.syndication.views.feed', {'feed_dict':feed_dict}),
)

That covers my URL wishes, and because I have named the URL patterns I can use that name in templates with the {% url %} template tag and in Python code using the reverse helper.

So naturally the feed classes in feeds.py look like this:

# myapp/feeds.py
from django.contrib.syndication.feeds import Feed
from django.core.urlresolvers import reverse
from django.utils.feedgenerator import Atom1Feed
from models import Tx


class LatestArrivals(Feed):
    """Produces an Atom feed of recent arrival tickets."""
    feed_type = Atom1Feed
    title = 'Arrivals'
    link = reverse('arrivals')
    subtitle = 'Most recent arrivals'

    def items(self):
        return Tx.objects.arrivals()[:10]


class LatestDepartures(Feed):
    """Produces an Atom feed of recent departure tickets."""
    feed_type = Atom1Feed
    title = 'Departures'
    link = reverse('departures')
    subtitle = 'Most recent departures'

    def items(self):
        return Tx.objects.departures()[:10]

Note I used reverse on the link attribute of each class so that I can define the URL in one place, the urls.py module, and a change there will be reflected in the feed’s link too.

But this doesn’t work! When Django imports my urls.py module, it imports LatestDepartures and LatestArrivals, and they in turn use reverse to find the named URL patterns – except those names aren’t defined until after urlpatterns has been defined in urls.py so Django throws an exception and never imports my urls.py module.

You could work around this either by defining your syndication feeds in an entirely different urls.py module. But you can also split up urlpatterns within the same module and import the feed classes after their named URL patterns have been defined.

Here’s the working urls.py module:

from django.conf.urls.defaults import *
from views import arrivals_list, departures_list


urlpatterns = patterns('',
    (r'^a/$', arrivals_list, {}, 'arrivals'),
    (r'^d/$', departures_list, {}, 'departures'),
)


from feeds import LatestArrivals, LatestDepartures
feed_dict = {'a': LatestArrivals, 'd': LatestDepartures}


urlpatterns += patterns('',
    (r'^(?P<url>[ad])/feed/$', 'django.contrib.syndication.views.feed', {'feed_dict':feed_dict}),
)