I’ve been wanting to pick up Web Framework for quite some time and have decided to make the plunge and starting learning Django (Python). Why I choose Django over others is a story for another day.
All to commonly when registering accounts on sites now a days, having a validated email for that account is vital. It may be for useful for password resets, notification and a whole barrage of other things, but is most common use is to activate an account. Here is my approach for achieving this in Django (I know some package already handle this) that not only takes advantage of Django built in features, but now adds on some custom functions. If your not at least familiar with the User class in Django, I suggest reading up on it first.
In the app that will handle users (I simply called mine ‘users‘) the first goal is to extend the User model provided by Django. This can be done simply by creating a new model, and linking it with a OneToOne field to our base User class.
from django.db import models
from django.contrib.auth.models import User
# - Extending the User class - #
class UserProfile(models.Model):
user = models.OneToOneField(User, related_name='profile')
activation_key = models.CharField(max_length=30)
#A small change I like to include, returns the username when viewed in Admin interface
def __unicode__(self):
return u"%s" % self.user
Great, now we can store additional information, most importantly an activation key, which will be generated when the users first registers. Next let’s setup our urls.py. We’re linking in two functions.
- A register page (register_page) where a user can register at www.example.com/register/
- An activation page (activate_user) which will be emailed to users linking them to www.example.com/activate/<activation key>.
from django.conf.urls.defaults import patterns, include, url
from users.views import *
urlpatterns = patterns('',
(r'^admin/', include(admin.site.urls)),
#Any other views you might have....
(r'^register/$', register_page),
(r'^activate/(?P[a-zA-Z0-9_.-]+)/', activate_user)
)
With registration, the first step is to create a form to handle our user’s registration details. To keep things organized, create a new file, foms.py in your app.
The form include some custom (overwritten) functions that are automatically called for cleaning the fields entered. These include…
- A password check, the user is asked to repeat his/her password and the function checks that match
- A username check, to ensure the is not only not taken, but doesn’t include invalid characters, such as spaces.
class RegistrationForm(forms.Form):
#Basic Account Information
username = forms.CharField(label=u'Username', max_length=30)
email = forms.EmailField(label=u'Email')
#Password Infomation
password = forms.CharField(
label=u'Password',
widget=forms.PasswordInput()
)
password_check = forms.CharField(
label=u'Password (Again)',
widget=forms.PasswordInput()
)
def clean_password_check(self):
if 'password' in self.cleaned_data:
password = self.cleaned_data['password']
password_check = self.cleaned_data['password_check']
if password == password_check:
return password_check
raise forms.ValidationError('Passwords do not match.')
#Check that the username does not exist and contains no invalid characters.
def clean_username(self):
username = self.cleaned_data['username']
if not re.search(r'^\w+$', username):
raise forms.ValidationError('Username can only contain '
'alphanumeric characters and the underscore.')
try:
User.objects.get(username=username)
except User.DoesNotExist:
return username
raise forms.ValidationError('Username is already taken.')
Next, moving to views.py we need a registration function and we want to include three key features…
- When initially registering, the user is set to not active
- An activation key is generated (comprising of random uppercase characters and numbers)
- An email is sent to the user with the activation key and URL to active their account (send_mail function). Note: Just make sure you have setup your settings.py to handle email.
Lets have a look at the code that allows us to achieve this.
from users.forms import *
from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.shortcuts import render_to_response
from django.http import Http404
from django.template import RequestContext
import random, string
def register_user(request):
if request.method == 'POST':
form = RegistrationForm(request.POST)
if form.is_valid():
#A user account is created from infomation passed through the forms
new_user = User.objects.create_user(
username=form.cleaned_data['username'],
password=form.cleaned_data['password'],
email=form.cleaned_data['email']
)
#The user account is set to not active
new_user.is_active = False
#We extend the User to our User Profiles and generate an 20 character activtion key
user_profile = UserProfile.objects.create(
user = new_user,
activation_key = ''.join(random.choice(string.ascii_uppercase + string.digits) for n in range(20))
)
#An email is sent too the User at the email they registered with
send_mail(
'Activation Code',
'Please visit http://example.com/activate/%s/ to active your account' % (user_profile.activation_key),
'registration@your-site.com',
['%s'] % new_user.email)
#Finally all updated details are saved
new_user.save()
user_profile.save()
#And the user is re-directed to the home page (best to put in a success page)
return HttpResponseRedirect('/')
else:
form = RegistrationForm()
variables = RequestContext(request, {
'form': form
})
return render_to_response(
'registration/register.html',
variables
)
Here is the corresponding html in registration/register.html. Django’s template tags handles forms so simply. The entire form can be expressed as {{ form.as_p }}. What a clean solution! Just for good measure I’ve thrown in a simple if not statement to make sure the user does not already have an account (by being logged in).
{% extends "base.html" %}
{% block title %}Registration{% endblock %}
{% block head %}Registration{% endblock %}
{% block content %}
{% if not user.is_authenticated %}</pre>
<form action="." method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Register?" /></form>
<pre>
{% else %}
<p>You are already registered</p>
{% endif %}
{% endblock %}
Great, now the user has been registered, a link has been sent his/her email. The format of the link is “www.example.com/activate/<activation_key>”. Where <activation_key> corresponds to account freshly registered. As shown before in our urls.py this activation key is captured, and passed to the function activate_user. The activate_user function does the following
- Uses the captured <activation_key> and looks up the user with that key
- If a user with that key exists, their account is activated.
- If the user doesn’t exist, a 404 page is page is thrown up saying no activation key found
- If the user is already active, a 404 page is thrown up saying that the account is already activate.
Now let’s have a look at the code over in views.py
def activate_user(request, activation_key): try: #First, the code tries to look up the user based on the activation key user = UserProfile.objects.get(activation_key=activation_key) #If found, and the user is not active, the user's account is activated. if user.is_active == False: user.is_active == True user.save() #Else, if the user is already active, an error page is passed else: raise Http404(u'Account already activated') #If no user is found with the activation key, an error page is passed except User.DoesNotExist: raise Http404(u'No activation key found') return render_to_response( 'registration/activation.html',)
That it. The function is short and to the point (as all good functions should be). Finally, all that is left to do is to create the corresponding template /registration/activation.html
{% extends "base.html" %}
{% block title %}Activation{% endblock %}
{% block head %}Activation{% endblock %}
{% block content %}
<p>This account is now activated</p>
{% endblock %}
Extremely simple. But remember, this page will only be reached if an account is activated. If you want to extend it you could easily pass a few variables into it. That it! I’ll be posting up another Django post next week. Let me know if this helped or what type of post you would be interested in seeing in the future?
If you’ve read this far, maybe consider following me on twitter.