Django Extending User model

Django only provides bare minimum fields in the User model to get you started, but it also gives you full power to extend the User model to suit your application needs.

Recall that by default User model contains the following fields:

  1. username
  2. first_name
  3. last_name
  4. email
  5. password
  6. last_login
  7. is_active
  8. is_staff
  9. is_superuser
  10. date_joined

The first step in extending a User model is to create a new model with all the additional fields you want to store. To associate our new model with the User model define a OneToOneField containing reference to the User model in our new model.

Open blog's models.py and modify Author model as follows:

TGDB/django_project/blog/models.py

...
from django.db import models
from django.contrib.auth.models import User

# Create your models here.

class Author(models.Model):

    ## required to associate Author model with User model (Important)
    user = models.OneToOneField(User, null=True, blank=True)

    ## additional fields
    phone = models.IntegerField(blank=True, default=1)    
    activation_key = models.CharField(max_length=255, default=1)
    email_validated = models.BooleanField(default=False)

    def __str__(self):
        return self.name

...

Here we have defined three extra fields, namely phone, activation_key and email_validated. Each of these fields does the following:

Field Description
activation_key It stores a random unique key sent during email verification. User must click on the link containing activation_key to activate the account.
email_validated The email_validated field stores boolean data i.e 0 or 1. If account is not validated it contains 0. Otherwise, 1.

In the terminal or command prompt, create new migration file by executing the following command:

(env) C:\Users\Q\TGDB\django_project>python manage.py makemigrations
Did you rename author.active to author.email_validated (a BooleanField)? [y/N] n

Migrations for 'blog':
  blog\migrations\0010_auto_20170916_2041.py:
    - Remove field active from author
    - Remove field created_on from author
    - Remove field email from author
    - Remove field last_logged_in from author
    - Remove field name from author
    - Add field activation_key to author
    - Add field email_validated to author
    - Add field phone to author
    - Add field user to author
(env) C:\Users\Q\TGDB\django_project>

When asked for Did you rename author.active to author.email_validated (a BooleanField)? [y/N], hit N or n.

Finally, commit the changes using python manage.py migrate command.

(env) C:\Users\Q\TGDB\django_project>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Rendering model states... DONE
  Applying blog.0010_auto_20170916_2041... OK

(env) C:\Users\Q\TGDB\django_project>

While we are at it, let's update __str__() and get_absolute_url() methods. At this point both of these method uses name field of the Author model. As there is no such field right now, if you try to http://127.0.0.1:8000/ or any other page which uses Author model in anyway you would get an error like this:

[img]

To fix this error update __str__() and get_absolute_url() methods to use username attribute of the User model instead of name.

...
    def __str__(self):
        return self.user.username

    def get_absolute_url(self):
        return reverse('post_by_author', args=[self.user.username])
...

Lets revise our cadmin's register() view to send email verification to the newly created account.

TGDB/django_project/cadmin/views.py

...
from django_project import helpers
from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.conf import settings
from django_project.helpers import generate_activation_key
...

def register(request):
    if request.method == 'POST':
        f = CustomUserCreationForm(request.POST)
        if f.is_valid():

            # send email verification now

            activation_key = helpers.generate_activation_key(username=request.POST['username'])

            subject = "TheGreatDjangoBlog Account Verification"

            message = '''\n
Please visit the following link to verify your account \n\n{0}://{1}/cadmin/activate/account/?key={2}
                        '''.format(request.scheme, request.get_host(), activation_key)            

            error = False

            try:
                send_mail(subject, message, settings.SERVER_EMAIL, [request.POST['email']])
                messages.add_message(request, messages.INFO, 'Account created! Click on the link sent to your email to activate the account')

            except:
                error = True
                messages.add_message(request, messages.INFO, 'Unable to send email verification. Please try again')

            if not error:
                u = User.objects.create_user(
                        request.POST['username'],
                        request.POST['email'],
                        request.POST['password1'],
                        is_active = 0
                )

                author = Author()
                author.activation_key = activation_key
                author.user = u
                author.save()

            return redirect('register')

    else:
        f = CustomUserCreationForm()

    return render(request, 'cadmin/register.html', {'form': f})

Here is rundown of the changes we have made in register() view:

  1. In line 15, we are creating an activation_key based upon the username entered using generate_activation_key() helper function. The generate_activation_key() function is defined in helpers.py file as follows:

    TGDB/django_project/django_project/helpers.py

     ...
     import hashlib
     from django.utils.crypto import get_random_string
     ...
    
     def generate_activation_key(username):
         chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
         secret_key = get_random_string(20, chars)
         return hashlib.sha256((secret_key + username).encode('utf-8')).hexdigest()
    
  2. In line 25, we are sending email verification using send_email() function. The syntax of send_mail() function is:

     send_mail(subject, message, sender, recipient_list)
    

    On success it returns 1, otherwise 0.

    If error is encountered while sending email verification we set error to True.

  3. If no error is encountered while sending email then we proceed to create new User. Notice how we are saving the additional User data into the database.

    In lines 33-38, we are using create_user() method to save built-in fields of User model. At this point, additional user data like phone, activation_key and email_validated is not yet saved. In lines 41 and 42, we are setting values to activation_key and user field. And in line 43, we are saving additional User data by calling save() method on the Author object.

Next, add another view called activate_account() in cadmin's views.py file, whose job is to set is_active field to True when visitor click on the email verification link in the email.

TGDB/django_project/cadmin/views.py

from django.shortcuts import redirect, get_object_or_404, reverse, Http404
...

def activate_account(request):
    key = request.GET['key']
    if not key:
        raise Http404()

    r = get_object_or_404(Author, activation_key=key, email_validated=False)
    r.user.is_active = True
    r.user.save()
    r.email_validated = True
    r.save()

    return render(request, 'cadmin/activated.html')

Create a new file named activated.html and add the following code to it:

TGDB/django_project/cadmin/templates/cadmin/activated.html

{% extends "cadmin/base.html" %}

{% block title %}
    Create Account - {{ block.super }}
{% endblock %}

{% block content %}

    <div class="logout">
        <p>Account activated! Please <a href="{% url 'login' %}">login</a></p>
</div>

{% endblock %}

Finally, add a URL pattern named activate just below the register URL pattern in cadmin's urls.py file as follows:

TGDB/django_project/cadmin/urls.py

...
urlpatterns = [   
    url(r'^register/$', views.register, name='register'),
    url(r'^activate/account/$', views.activate_account, name='activate'),
   ...

]

Visit http://127.0.0.1:8000/cadmin/register/ and create a new user.

If registration succeeds, you will be greeted with a success message and email containing a verification link will be sent to the email id provided at the time of registration.

user-creation-success-2.png

email-verification-link-GTLINV.png

To verify your account click on the verification link and you will be greeted with "Account successfully verified. Please login." message.

account-activated.png

You account is now active and can be used to login. Visit http://127.0.0.1:8000/cadmin/login/ and login with your newly created account.

If user registration failed due to duplicate username/email or unmatching password the form will display validation errors like this:

user-registration-validation-errors.png

In case, Django unable to send email verification then you will get an error like this:

email-verification-failed-EVDVFE.png