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 a 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.urls import reverse
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
    activation_key = models.CharField(max_length=255, default=1)
    email_validated = models.BooleanField(default=False)

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


class Category(models.Model):
    #...

Here we have defined two extra fields, namely 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. The 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 an account is validated it contains 1. Otherwise, 0.

Before we run makemigration command comment out the AuthorAdmin class and also remove it from the admin.site.register() function. At this point, blog's admin.py should look like this:

TGDB/django_project/blog/admin.py

from django.contrib import admin
from . import models

# Register your models here.


# class AuthorAdmin(admin.ModelAdmin):
#     list_display = ('name', 'email', 'created_on')
#     search_fields = ['name', 'email']
#     ordering = ['-name']
#     list_filter = ['active']
#     date_hierarchy = 'created_on'


class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'pub_date', 'author', 'category',)
    search_fields = ['title', 'content']
    ordering = ['-pub_date']
    list_filter = ['pub_date']
    date_hierarchy = 'pub_date'
    # filter_horizontal = ('tags',)
    raw_id_fields = ('tags',)
    # prepopulated_fields = {'slug': ('title',)}
    readonly_fields = ('slug',)
    fields = ('title', 'slug', 'content', 'author', 'category', 'tags',)


class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug',)
    search_fields = ('name',)


class TagAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug',)
    search_fields = ('name',)


class FeedbackAdmin(admin.ModelAdmin):
    list_display = ('name', 'email', 'subject','date',)
    search_fields = ('name', 'email',)
    date_hierarchy = 'date'


admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Category, CategoryAdmin)
# admin.site.register(models.Author, AuthorAdmin)
admin.site.register(models.Author)
admin.site.register(models.Tag, TagAdmin)
admin.site.register(models.Feedback, FeedbackAdmin)

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>

Let's update our cadmin's register() view to send email verification to the newly created accounts.

TGDB/django_project/cadmin/views.py

#...
from .forms import CustomUserCreationForm
from django_project import helpers
from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.conf import settings

#...
def login(request, **kwargs):
    #...


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 18, 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

    from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
    import hashlib
    from django.utils.crypto import get_random_string
    
    def pg_records(request, list, num):
        #...
    
    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 29, 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 an error is encountered while sending email verification we set error to True.

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

    In lines 37-42, we are using create_user() method to save built-in fields of User model. At this point, additional user data like activation_key and email_validated is not yet saved into the database. In lines 45 and 46, we are setting values to activation_key and user field. And in line 47, 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 and email_validated field to True when the user clicks on the verification link in the email.

TGDB/django_project/cadmin/views.py

from django.shortcuts import redirect, get_object_or_404, reverse, Http404
#...
from django.conf import settings
#...

def register(request):
    #...

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 template activated.html for activate_account() view 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 above the register URL pattern in cadmin's urls.py file as follows:

TGDB/django_project/cadmin/urls.py

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

]

Our user registration form is now ready. 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.

Selection_031

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.

Selection_034

Your 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:

Selection_035

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

Selection_036

Removing obsolete data #

If you now visit post add or update page you will notice an error like this:

Selection_041

The problem is that in the process of committing changes to the Author model our last migration also has removed all the authors from the blog_authors table. Only the authors which we created via User Registration form after updating Author models are valid.

Selection_039

Selection_040

To fix the issue, designate all the posts to author(s) which is connected to the User model. In my case, I am designating all post to an author whose username is charmander. To do so, execute the following SQL.

update blog_post
set author_id = 12

The exact author_id may vary.

Finally, delete all the posts authors which are not connected to the User model.

PRAGMA foreign_keys = OFF;

delete from blog_author
where user_id is null;

PRAGMA foreign_keys = ON;

While we are at it, let's update author of category and tags too:

-- code to update categories

update blog_category
set author_id = 12;

-- code to update tags

update blog_tag
set author_id = 12;

Visit post add or post update page and the error would have gone.

Note: To checkout this version of the repository type git checkout 33a.