26Jun

Dynamic names as first-level URL path objects in Django

Posted by Elf Sternberg as django, programming, python, web development

At del.icio.us (and some other sites, but I’ll use del.icio.us as my example), one of the most interesting features is that your username is one of the first-level “commands” you can send to the application: “http://del.icio.us/elfsternberg” is a valid URL, and points to my collection of bookmarks.

At the same, there are other first-level commands you might send to del.icio.us, including /save, /tag, /user, and /network, and /url. In Django, all of these commands would be rooted in url.py.

I’m going to show you a few very simple ways to make the magic of del.icio.us work in Django, and (most importantly) show how to validate new users at registration time to make sure that their usernames do not mask, and are not masked by, your otherwise RESTful URLs.

Using the Resolver to ensure uniqueness

I’m going to assume you know enough about Django to set up a demonstration sever. If you don’t, please consult the Django documentation at the Django Project.

The basic premise here is that you have some basic RESTFUL paths, and then you have your username-based paths. Let’s assume you’re using django-registration, since the real difficulty here is ensuring no invalid slugs get past your registration engine. For my purposes, the project is named “demonstration” and the application “demo”. Here is what you add to urls.py

<urls.py>=
from django.conf.urls.defaults import *
from django.contrib.auth.views import *
from django.core.urlresolvers import reverse

# Just two pages to show what we mean.
from demo.index import intro, index

# This is explained below.
from demo.forms import NewRegistrationForm

urlpatterns = patterns(",
    # Ordinary URLS understood by the system.
    url(r'^$', index, name="index"),
    url(r'^index/$', index, name="index"),
    url(r'^intro/$', intro, name="intro"),

    # Override the registration application's register method to
    # include a registration form with a URL checker.
    url(r'^accounts/register/$',
        'registration.views.register',
        { 'form_class': NewRegistrationForm },
        name = 'registration_register'),

    # URLs driven by the bookmarking application
    url(r'^(?P<user_slug>\w+)/$', user, name="user"),
    url(r'^(?P<user_slug>\w+)/(?P<object_slug>\d+)$',
        bookmark, name="addnew"),

The big keys here are the NewRegistrationForm and the URLs in which the user_slug (if you don’t know what a slug is, you can consult the Django glossary at , but the short form is this: a slug is a URL-ready string version of a label, all-lowercase, alphanumeric only, with spaces either eliminated or replaced with hyphens. If my name is “Elf Sternberg”, typical slugs would be elfsternberg or elf-sternberg. Using slugs instead of object IDs has become popular as a way of assisting with mnemonic URLs. For more, see “Cool URLs don’t change“) is exploited to find the “hub” view of the user’s experience.  Different views are provided at the same URL for those who are logged in and viewing their own page, and those who are viewing pages belonging to someone other than themselves.

Writing the user and bookmark slugs should be trivially easy. The question here becomes, how do you prevent users from choosing a name like “index” or “intro,” names that would be masked by the command syntax, since Django resolves URLs in the order in which URL patterns appear in the urls.py file

Above, in urls.py, I mentioned that we had imported NewRegistrationForm, a newly defined object within the application. I also told django-registration to use NewRegistrationForm as the form to show when going to the registration page.

NewRegistrationForm inherits from the base registration form and adds another layer of validation: confirm for me that the username being added would not conflict with an existing URL that already resolves within our application. To do that, we just use the resolve function provided by Django, by defining our own forms file under the demo/ directory:

<forms.py>=
from registration.forms import RegistrationForm
from django.forms import ValidationError
from django.core.urlresolvers import resolve, Resolver404
from urlparse import urlparse

class NewRegistrationForm(RegistrationForm):

    # Ensures than any usernames added will not
    # conflict with existing commands.

 def clean_username(self):
        username = super(NewRegistrationForm, self).clean_username()
        try:    resolve(urlparse('/' + username + '/')[2])
        except Resolver404, e:
            return username

        raise ValidationError(_(u'This username does not create '
                                u'a valid URL.  Please choose '
                                u'another'))

After importing all the necessary tools, I create a new class and override the method clean_username, which first invokes the parent class machinery for validating the username, and then attempts to resolve that username against the existing application. If resolve throws the Resolver404 exception, then we know that the username did not resolve, and is therefore valid to use! Otherwise, we raise a validation error. I always assume the presence of a gettext handler (the underscore function) and the use of unicode inside the application.

A couple of style notes: I assume gettext is installed as the underscore function, and I enjoy exploiting Python’s behavior of concatening adjacent strings into a single expression.  That’s what the text in the exception string is doing.

Big Fat Caveat

Make sure you have a very clear vision of your product’s future evolution, and that all of the commands you anticipate providing in the future have been allocated or reserved before you let your users have their hands on the system. Users are clever and will reserve usernames that might mask commands that you’ll want. Your only alternatives are to negotiate with the user for a change of name, or get out the thesaurus. Neither is terrific fun.

5 Responses to Dynamic names as first-level URL path objects in Django

Buckley

December 8th, 2009 at 7:27 am

I don’t understand how this would work. Won’t the resolve call in the try block return the user url pattern in the case where the registration is valid?

Elf Sternberg

December 8th, 2009 at 8:00 am

Yes, and that’s the point! If the resolve block doesn’t throw an exception, the resolution works, meaning you’ve chosen a name that conflicts with (a) a reserved URL for the application or (b) a username already in use by someone else. The user must pick something else.

Does that help?

Buckley

December 8th, 2009 at 8:36 am

What I’m trying to say is the exception will never be raised. Even if the user chooses a valid username, it will still match the ‘user’ pattern regardless of whether or not the username has been picked by someone else.

Philip Gatt

April 26th, 2011 at 10:14 pm

Instead of checking for an error, check that the desired url is the catchall. In my app I have this and it’s working:

def clean_username(self):
username = self.cleaned_data['username'].lower()
try:
r = urlresolvers.resolve(‘/{0}/’.format(username))
except urlresolvers.Resolver404:
raise RuntimeError(“Username should match a url-check routes file”)
else:
if r.url_name != ‘room’:
raise forms.ValidationError(‘Username is unavailable’)
return username

Dynamic names as first-level URL path objects in Django | BlogoSfera

October 2nd, 2013 at 6:02 am

[...] solution I found is this but the solution is not correct. The validation [...]

Comment Form

Recent Comments