Django tips: user registration

Published September 2, 2006. Filed under: Django.

One of the most common and, generally, most successful models for letting people sign up for user accounts requires the user to go through some form of “activation”; usually it looks something like this:

  1. User fills out a form with a username, password and email address.
  2. User gets an email with a “confirmation link” which must be clicked to activate the account.
  3. User clicks the link and the account becomes active; then they log in normally.

So let’s look at how to do this with Django.

Also, before we dive in, let me note that this one’s still a work in progress. The question of how to do this has come up a lot, and it’s something I’ve been working on for a personal project, so this is basically what I’m using right now — I haven’t tested it thoroughly, though, so if you spot a problem please leave a comment.

Update: if you’re interested in the mechanics of how registration works, keep reading. If you’re looking for code, I’ve cleaned this up considerably and released it under an open-source license; check out the django-registration project on Google Code if you want to play around with it.

Set up a user profile

In order to do this effectively, we’ll need to generate some sort of unique key which can be used to activate the account, and we’ll need somewhere to store it. The obvious place is in a model which is somehow tied to the User model, probably some sort of “user profile” and, as luck would have it, you’ll probably want to be setting something like that up anyway — a customizable user profile is a nice feature to have regardless.

So here’s a very simple “user profile” model; you’d probably want to add more fields to it, but this has the bare minimum we need to do user registration:

from django.db import models
from django.contrib.auth.models import User
 
class UserProfile(models.Model):
    user = models.OneToOneField(User)
    activation_key = models.CharField(maxlength=40)
    key_expires = models.DateTimeField()

Once that model is installed, we can open up our settings file and specify it as the value of the setting AUTH_PROFILE_MODULE, so that Django will set up some conveniences for us.

OK, now how do we register an account?

Ordinarily when creating an instance of a model, it’d be easiest just to use the create_object generic view, but in this particular case that won’t be sufficient for a couple of reasons:

  1. It would create an active account, which we don’t want — the User model lets us mark users active or inactive with a BooleanField called is_active, but that field defaults to True.
  2. It would require the user to input a salted hashed password, which is horribly unfriendly.
  3. It wouldn’t give us any way to check the user’s password for typos (say, by having them enter it twice).
  4. It wouldn’t offer us any way to email the user a confirmation link.

So we’ll want to write our own view. The password-related problems described above could be worked around by using UserCreationForm, a custom manipulator provided by the auth application which provides two password fields and checks that they match, then does the salting and hashing automatically before saving (this is how the admin lets you easily create a new user, incidentally — it uses a special-case view that relies on UserCreationForm). But UserCreationForm doesn’t include a field for the email address, so we’d have no way of asking for it or sending a confirmation email.

So let’s write our own manipulator while we’re at it. We’ll need that to be defined before we can call on it from a view, so we’ll write it first. Here’s the code:

from django import forms
from django.core import validators
from django.contrib.auth.models import User
 
class RegistrationForm(forms.Manipulator):
    def __init__(self):
        self.fields = (
            forms.TextField(field_name='username',
                            length=30, maxlength=30,
                            is_required=True, validator_list=[validators.isAlphaNumeric,
                                                              self.isValidUsername]),
            forms.EmailField(field_name='email',
                             length=30,
                             maxlength=30,
                             is_required=True),
            forms.PasswordField(field_name='password1',
                                length=30,
                                maxlength=60,
                                is_required=True),
            forms.PasswordField(field_name='password2',
                                length=30, maxlength=60,
                                is_required=True,
                                validator_list=[validators.AlwaysMatchesOtherField('password1',
                                                                                   'Passwords must match.')]),
            )
    
    def isValidUsername(self, field_data, all_data):
        try:
            User.objects.get(username=field_data)
        except User.DoesNotExist:
            return
        raise validators.ValidationError('The username "%s" is already taken.' % field_data)
    
    def save(self, new_data):
        u = User.objects.create_user(new_data['username'],
                                     new_data['email'],
                                     new_data['password1'])
        u.is_active = False
        u.save()
        return u

This may look somewhat complex, but it’s not. Most of it will make sense if you’ve read through the documentation on manipulators, and it’s really just a slightly modified version of UserCreationForm designed to suit the needs of web-based registration.

Here’s how it breaks down:

  1. The __init__ method sets up the fields we care about: one for username, one for email and two copies of the password. We use the AlwaysMatchesOtherField validator on the second password field to tell Django to make sure the password is entered the same each time.
  2. The isValidUsername method is a custom validator, designed to give us immediate notice if the username is already taken.
  3. The save method calls User.objects.create_user; the User model has a custom manager which defines this method to easily handle creating the hashed password, and all we have to do is give it a username, email address and plain-text password — it will hand us back a valid User object. Then we set is_active to False and save the new user.

OK, now we can sign up

Now that we’ve got a manipulator that will do what we want, we can write a simple registration view that lets new users sign up. First we import a few things we’ll need:

import datetime, random, sha
from django.shortcuts import render_to_response, get_object_or_404
from django.core.mail import send_mail

We’ll also need the UserProfile model and the RegistrationForm manipulator we just created, but since they’ll live inside our application the exact import statements will depend on the name and layout of the app. When you’re defining custom manipulators, it’s often a good idea to put them in a file named forms.py inside your application; assuming we’ve done that here, the imports would look like this:

from myproject.myapp.models import UserProfile
from myproject.myapp.forms import RegistrationForm

Now let’s look at the view for registration:

def register(request):
    if request.user.is_authenticated():
        # They already have an account; don't let them register again
        return render_to_response('register.html', {'has_account': True})
    manipulator = RegistrationForm()
    if request.POST:
        new_data = request.POST.copy()
        errors = manipulator.get_validation_errors(new_data)
        if not errors:
            # Save the user                                                                                                                                                 
            manipulator.do_html2python(new_data)
            new_user = manipulator.save(new_data)
            
            # Build the activation key for their account                                                                                                                    
            salt = sha.new(str(random.random())).hexdigest()[:5]
            activation_key = sha.new(salt+new_user.username).hexdigest()
            key_expires = datetime.datetime.today() + datetime.timedelta(2)
            
            # Create and save their profile                                                                                                                                 
            new_profile = UserProfile(user=new_user,
                                      activation_key=activation_key,
                                      key_expires=key_expires)
            new_profile.save()
            
            # Send an email with the confirmation link                                                                                                                      
            email_subject = 'Your new example.com account confirmation'
            email_body = "Hello, %s, and thanks for signing up for an \                                                                                                     
example.com account!\n\nTo activate your account, click this link within 48 \                                                                                               
hours:\n\nhttp://example.com/accounts/confirm/%s" % (
                new_user.username,
                new_profile.activation_key)
            send_mail(email_subject,
                      email_body,
                      'accounts@example.com',
                      [new_user.email])
            
            return render_to_response('register.html', {'created': True})
    else:
        errors = new_data = {}
    form = forms.FormWrapper(manipulator, new_data, errors)
    return render_to_response('register.html', {'form': form})

Here’s what’s going on in that view:

  1. First we use is_authenticated to see if the user is logged in already — if they are, they have an account and shouldn’t sign up for a new one, so we immediately stop and return a response. The template register.html should have some way of checking for the already_has_account context variable and show an appropriate message.
  2. Otherwise we instantiate a RegistrationForm to use during the sign-up processing.
  3. If the request was a POST, we feed it to the manipulator and check for validation errors.
  4. If there were no validation errors, then we can go through with saving the new account and sending a confirmation email.
  5. If there were errors, or if the request wasn’t a POST, we hand off to the template with a form to display.

Now, most of the interesting stuff happens in step 4, where we save the new account, so let’s walk through that step by step:

  1. We convert the submitted information to Python values, and save the new user account. That hands us back a brand-new, but inactive, user.
  2. Then we create a suitably random activation key; this is why we imported the random and sha modules — we get a random number and create a SHA hash from it, grabbing the first few characters to use as a “salt”; then we mash the new account’s username onto the end of the “salt” and generate a SHA hash of that, which will be the final activation key.
  3. We use timedelta to get a date value two days into the future — we’ll use this to expire the activation key after 48 hours.
  4. We create and save a new UserProfile, tied to the User we just created, and give it the activation key and expiration date we’ve just generated.
  5. We build up a subject and body for an email message, then send it off to the person who just signed up by using Django’s built-in send_mail function (note that to be truly portable, the email message should probably actually get information about the site’s name and domain from django.contrib.sites, and that send_mail takes a list of email addresses to send to).

That’s a whole lot of stuff, without a whole lot of code :)

Confirmation time

Now the only thing left to write is a view which can activate the new account:

def confirm(request, activation_key):
    if request.user.is_authenticated():
        return render_to_response('confirm.html', {'has_account': True})
    user_profile = get_object_or_404(UserProfile,
                                     activation_key=activation_key)
    if user_profile.key_expires < datetime.datetime.today():
        return render_to_response('confirm.html', {'expired': True})
    user_account = user_profile.user
    user_account.is_active = True
    user_account.save()
    return render_to_response('confirm.html', {'success': True})

This view assumes that you’ve set up your URLs to grab the part which contains the activation key and pass it as an argument.

Here’s how it works from there:

  1. Once again, we check to make sure the user’s not already logged in — if they are, there’s no point trying to activate a new account for them.
  2. We use the shortcut get_object_or_404 to see if there’s a UserProfile matching this activation key — if not, it’ll automatically send a “page not found” response.
  3. We check whether the activation key has expired; if so, we jump straight to the response and let the template know it should display an appropriate message.
  4. If the key isn’t expired, we grab the User object associated with this UserProfile, and flip its is_active field to True.
  5. Finally, we return a response, telling the template that the activation was successful. It’d probably be a good idea for the template to show a link to a login page at this point (or, alternatively, you could import and use the convenience function django.contrib.auth.login to automatically log the user in).

And we’re done

Now we’ve got a fully functional user-registration system, with confirmation emails. We did have to write some code, but there’s not a whole lot of it and it’s all fairly straightforward.

If, like more than a couple people who’ve asked questions about user registration on the Django mailing lists and in IRC, you’ve been looking for a way to do this in Django, feel free to take this code and adapt it to your needs — I can’t guarantee that it’ll work for what you want, but if nothing else it should give you a good start.