This tutorial demonstrates the entire user-authentication lifecycle in Django, with testing at every step:

Create an account, both with and without email confirmation Login, including a forgot-my-password reset (via email), and a keep-me-logged-in checkbox A page viewable when logged out, but containing extra information when logged-in A page viewable only when logged in: Your user “profile”. Change your password Logout Delete your account

[The chapters on creating and deleting an account (chapters nine and ten), and changing-your-password (chapter eight) are not yet written. Resetting your password is in chapter seven.]

[TOC: one, two, three, four, five, six, seven, eight, nine, ten]

The goal of this tutorial is to require as trivial a website as possible, before proceeding onto authentication. In contrast, the How To Tango With Django tutorial does not address authentication until chapter eight, and Mike Hibbert’s video tutorial doesn’t talk about it until chapter nine. It’s not possible to start in the middle, as they both expect that all previous steps were followed. (Other available tutorials.)

Warnings:

The testing code increases the amount of code, and the number of steps in this tutorial, by a lot. So creating the demo website is more substantial than I just implied. However, if you did skip the testing–which you shouldn’t–implementing the website itself would be much faster. Were proper testing in either the Tango or Hibbert tutorials, they would be a whole lot longer.

The one type of testing I can’t demonstrate in this tutorial is end-user simulation/integration testing with Selenium. My webserver is text-only. This leaves the JavaScript portions of this tutorial, such as client-side make-sure-the-passwords-match verification, untested.

The trivial website: Screenshots

Before doing authentication, we need to create something. Let’s take a look at that something before actually building it. It will contain these two simple views:

The main "aggregate" page which is publicly viewable, but contains extra information for logged-in users, including a link to their private profile page:

The profile page, which displays every non-password field in the User model, plus the one extra field that makes up the entirety of our model: birth year.

Installation

Here is my setup. The only differences for this tutorial are:

The virtualenv base directory is

/home/myname/django_auth_lifecycle/djauth_venv/

The project base directory is

/home/myname/django_auth_lifecycle/djauth_root/

The reason for this structure is so that the entire django_auth_lifecycle directory can be a Git repository. It is also where I place non-Django files, such as some scripts, some personal-only files, the wordpress posts, including their java builders. In actuality, each post is its own repository. When one part is done–or if there’s a bug–all changes are copied over to all future parts. Attempting to have all posts in one monster repository is impossible, given my current beginner-level Git skills.

The steps I took, which you will need to tailor to your environment:

mkdir -p /home/myname/django_auth_lifecycle/djauth_venv/ sudo virtualenv -p /usr/bin/python3.4 /home/myname/django_auth_lifecycle/djauth_venv/ source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate sudo /home/myname/django_auth_lifecycle/djauth_venv/bin/pip install django sudo /home/myname/django_auth_lifecycle/djauth_venv/bin/pip install gunicorn sudo /home/myname/django_auth_lifecycle/djauth_venv/bin/pip install psycopg2 (this step has a lot of output) sudo chown -R myname /home/myname/django_auth_lifecycle/

Install a new Django project

Start your virtualenv :

source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate (exit it with deactivate ) Create the project directory:

mkdir /home/myname/django_auth_lifecycle/djauth_root/ Create the project (this is a long command that belongs on a single line) :

django-admin.py startproject django_auth_lifecycle /home/myname/django_auth_lifecycle/djauth_root Create the sub-application: cd /home/myname/django_auth_lifecycle/djauth_root/ python manage.py startapp auth_lifecycle

This and the previous command create the following (items unused by this tutorial are omitted): $ tree /home/myname/django_auth_lifecycle/djauth_root/ +-- auth_lifecycle | +-- admin.py | +-- models.py | +-- views.py +-- django_auth_lifecycle | +-- settings.py | +-- urls.py +-- manage.py In

/home/myname/django_auth_lifecycle/djauth_root/django_auth_lifecycle/settings.py Add 'auth_lifecycle' to INSTALLED_APPS Configure your database by overwriting the current value with DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'database_name_here', 'USER': 'database_username_here', 'PASSWORD': 'database_user_password_goes_here', 'HOST': "localhost", # Empty for localhost through domain sockets or # '127.0.0.1' for localhost through TCP. 'PORT': '', # Set to empty string for default. } } If you were not yet prompted to create a superuser, do it now: cd /home/myname/django_auth_lifecycle/djauth_root/ python manage.py createsuperuser The rest of this tutorial expects the superuser’s username and password to both be "admin"

The model

The only thing in our model is the user’s year-of-birth, which will be stored in a UserProfile model that is linked to the default Django User model. Although I’ve added in some validation, a simpler alternative without it is below.

Replace the contents of

/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/models.py

with

""" Defines a single extra user-profile field for the user-authentication lifecycle demo project: - Birth year, which must be between <link to MIN_BIRTH_YEAR> and <link to MAX_BIRTH_YEAR>, inclusive. """ from datetime import datetime from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models OLDEST_EVER_AGE = 127 #:Equal to `127` YOUNGEST_ALLOWED_IN_SYSTEM_AGE = 13 #:Equal to `13` MAX_BIRTH_YEAR = datetime.now().year - YOUNGEST_ALLOWED_IN_SYSTEM_AGE """Most recent allowed birth year for (youngest) users.""" MIN_BIRTH_YEAR = datetime.now().year - OLDEST_EVER_AGE """Most distant allowed birth year for (oldest) users.""" def _validate_birth_year(birth_year_str): """Validator for <link to UserProfile.birth_year>, ensuring the selected year is between <link to OLDEST_EVER_AGE> and <link to MAX_BIRTH_YEAR>, inclusive. Raises: ValidationError: When the selected year is invalid. - https://docs.djangoproject.com/en/1.7/ref/validators/ I am a recovered Hungarian Notation junkie (I come from Java). I stopped using it long before I started with Python. In this particular function, however, because of the necessary cast, it's appropriate. """ birth_year_int = -1 try: birth_year_int = int(str(birth_year_str).strip()) except TypeError: raise ValidationError(u'"{0}" is not an integer'.format(birth_year_str)) if not (MIN_BIRTH_YEAR <= birth_year_int <= MAX_BIRTH_YEAR): message = (u'{0} is an invalid birth year.' u'Must be between {1} and {2}, inclusive') raise ValidationError(message.format( birth_year_str, MIN_BIRTH_YEAR, MAX_BIRTH_YEAR)) #It's all good. class UserProfile(models.Model): """Extra information about a user: Birth year. ---NOTES--- Useful related SQL: - `select id from auth_user where username <> 'admin';` - `select * from auth_lifecycle_userprofile where user_id=(x,x,...);` """ # This line is required. Links UserProfile to a User model instance. user = models.OneToOneField(User, related_name="profile") # The additional attributes we wish to include. birth_year = models.IntegerField( blank=True, verbose_name="Year you were born", validators=[_validate_birth_year]) # Override the __str__() method to return out something meaningful def __str__(self): return self.user.username

Register it into the admin app by replacing the contents of

/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/admin.py

with

from django.contrib import admin from .models import UserProfile admin.site.register(UserProfile)

and then sync it to the database:

source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate cd /home/myname/django_auth_lifecycle/djauth_root/ python manage.py makemigrations python manage.py migrate

The same model with no validation:

""" Defines a single extra user-profile field for the user-authentication lifecycle demo project: Birth year. There is no validation on this field. """ from django.contrib.auth.models import User from django.db import models class UserProfile(models.Model): """ Extra information about a user: Birth year. ---NOTES--- Useful related SQL: - `select id from auth_user where username <> 'admin';` - `select * from auth_lifecycle_userprofile where user_id=(x,x,...);` """ # This line is required. Links UserProfile to a User model instance. user = models.OneToOneField(User, related_name="profile") # The additional attributes we wish to include. birth_year = models.IntegerField( blank=True, verbose_name="Year you were born") # Override the __str__() method to return out something meaningful def __str__(self): return self.user.username

Utilities needed by future tests

There’s nothing to test yet. However, we can already create some very useful utilities for future tests: creating test-users in bulk, logging a user in, finding specific text in the html, and debugging. This is also a good spot to place some more generic testing documentation.

The tests require Factory Boy to create its demo data (instead of creating it manually). To install it:

source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate sudo /home/myname/django_auth_lifecycle/djauth_venv/bin/pip install factory_boy

Save the following as

/home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/test__utilities.py

""" Utilities used by testing code throughout the authentication-lifecycle and testing tutorial. DEPENDS ON TEST: *nothing* (must not depend on any test_*.py file) DEPENDED ON TEST: test__profile.py --- Generic information on running tests --- To run a single test: 1. source /home/myname/django_files/django_auth_lifecycle/djauth_venv/bin/activate 2. cd /home/myname/django_files/django_auth_lifecycle/djauth_root/ 3. python -Wall manage.py test auth_lifecycle.test__file_name To run all tests: python -Wall manage.py test auth_lifecycle Running tests documentation: - https://docs.djangoproject.com/en/1.7/topics/testing/overview/#running-tests Information on '-Wall' is at the bottom of that same section. If the output is too verbose, try it again without '-Wall'. If a test fails because the test database cannot be created, grant your database user creation privileges: - http://dba.stackexchange.com/questions/33285/how-to-i-grant-a-user-account-permission-to-create-databases-in-postgresql pylint auth_lifecycle.test__utilities > pylint_output.txt pylint auth_lifecycle.test__view_birth_stats > pylint_output.txt pylint auth_lifecycle.test__view_user_profile > pylint_output.txt """ from .models import MIN_BIRTH_YEAR from auth_lifecycle.models import UserProfile from django.contrib.auth.models import User from django.test import TestCase import factory TEST_USER_COUNT = 5 """The number of test users to create. Equal to `5`.""" TEST_PASSWORD = 'password123abc' """The password shared by all test users. Equal to `'password123abc'`.""" class UserProfileFactory(factory.django.DjangoModelFactory): """ Creates `UserProfile`-s, where each user has a unique birth year, starting with <link to .models.MIN_BIRTH_YEAR>. *Warning*: Creating more than MAX_BIRTH_YEAR - MIN_BIRTH_YEAR users will cause a ValidationError. """ #Uncommenting this line would allow you to directly create a #UserProfile, which would then automatically create a User. #- Docs: http://factoryboy.readthedocs.org/en/latest/reference.html#subfactory #user = factory.SubFactory('auth_lifecycle.test__utilities.UserFactory', profile=None) class Meta: model = UserProfile #factory.Sequence always starts at one. This starts it at #MIN_BIRTH_YEAR. #http://factoryboy.readthedocs.org/en/latest/reference.html#sequence #http://stackoverflow.com/questions/15402256/how-to-pass-in-a-starting-sequence-number-to-a-django-factoryboy-factory birth_year = factory.Sequence(lambda n: n + MIN_BIRTH_YEAR - 1) class UserFactory(factory.django.DjangoModelFactory): """ Creates `User`-s and its corresponding `UserProfile`-s. Each user has the same attributes, but with a unique sequence number, starting with one. See <link to TEST_PASSWORD>. """ class Meta: model = User #Automatically create a profile when the User is created. #- Docs: http://factoryboy.readthedocs.org/en/latest/reference.html?highlight=subfactory#relatedfactory profile = factory.RelatedFactory(UserProfileFactory, 'user') username = factory.Sequence(lambda n: 'test_username{}'.format(n)) first_name = factory.Sequence(lambda n: 'test_first_name{}'.format(n)) last_name = factory.Sequence(lambda n: 'test_last_name{}'.format(n)) email = factory.Sequence(lambda n: 'test_email{}@example.com'.format(n)) #http://factoryboy.readthedocs.org/en/latest/reference.html#postgenerationmethodcall #See Django mention at the bottom of that documentation section. password = factory.PostGenerationMethodCall('set_password', TEST_PASSWORD) def create_insert_test_users(): """ Insert <link to TEST_USER_COUNT> test users into the database. I don't understand why, but even though this is called for every test, via `setUp`, this does *not* create more than `TEST_USER_COUNT` users. Use the debugging statements to prove this. """ #print('a User.objects.count()=' + str(User.objects.count())) #http://factoryboy.readthedocs.org/en/latest/reference.html?highlight=create#factory.create_batch UserFactory.create_batch(TEST_USER_COUNT) #print('b User.objects.count()=' + str(User.objects.count())) def login_get_next_user(test_instance): """ Log in the next test user, assert it succeeded, and return the `User` object. """ test_instance.client.logout() test_user = UserFactory() #debug_test_user(test_user, prefix='Attempting to login:') did_login_succeed = test_instance.client.login( username=test_user.username, password=TEST_PASSWORD) test_instance.assertTrue(did_login_succeed) return test_user def assert_attr_val_in_content( test_instance, attribute_name, expected_value, page_content_str): """A specific attribute should be somewhere in the html.""" #print('assert_attr_val_in_content: expected_value=' + expected_value) test_instance.assertTrue(str(expected_value) in page_content_str) def debug_test_user(test_user, prefix=''): """ Print all user attributes to standard out, except password. Parameters: - prefix: Defaults to `''`. If not the empty string, printed before the user information """ if prefix is not '': print(prefix) profile = test_user.profile print('test_user.id=' + str(test_user.id)) print(' username=' + test_user.username + ', password=' + TEST_PASSWORD) print(' first_name=' + test_user.first_name + ', last_name=' + test_user.last_name) print(' email=' + test_user.email) print(' profile=' + str(profile)) print(' profile.birth_year=' + str(profile.birth_year))

—

That’s it for now.

In the next post, well implement and test the first of two views: The private user-profile page.

[TOC: one, two, three, four, five, six, seven, eight, nine, ten]

At this point, it would be a good idea to backup your files.

…to be continued…

(cue cliffhanger segue music)