The full user-authentication lifecycle in Django, with testing at every step — The step-by-step tutorial I wish I had (part four)

In parts one, two, and three, we set up the model and our trivial website. Today we’ll be creating our logout functionality, and the first of four versions of the login page. We’ll start with a plain, no-frills login page, and then in the next three posts, the following functionality will be added:

1. A “remember me” checkbox, so the user’s login session is remembered for two weeks. When unchecked, the session is forgotten at browser close.
2. Some basic client-side validation, to ensure the lengths of the username and password are between the expected minimum and maximum, to prevent hitting the server when obviously incorrect.
3. An “I forgot my password” link, so the user’s password is reset via an email message and simple reset form.

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

Warning: The server as set up in part one of this tutorial is insecure (http only). Please don’t place an actual login form (or any form with sensitive information) on anything except an encrypted (https) server.

The template

Save the following as
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/templates/registration/login.html

<P>...{% trans "I forgot my password..., ...Create a new account" %}...</P>
<!DOCTYPE html>
<html lang="en">
<HTML><HEAD>
    <TITLE>Login</TITLE>
    <!-- The following line makes this page pleasant to view on any device. -->
    <meta name="viewport" content="width=device-width" />
</HEAD>

<BODY>

<H1>Login</H1>

{% if form.errors %}
<P>...{% trans "I forgot my password..., ...Create a new account" %}...</P>

{% endif %}

<form method="post" action="{% url 'login' %}">
   {% csrf_token %}
   <table>
      <tr>
          <td>{{ form.username.label_tag }}</td>
          <td>{{ form.username }}</td>
      </tr>
      <tr>
          <td>{{ form.password.label_tag }}</td>
          <td>{{ form.password }}</td>
      </tr>
   </table>

   <input type="submit" value="login" />
   <input type="hidden" name="next" value="{% url 'main_page' %}" />
</form>

<P>{% trans "...I forgot my password..., ...Create a new account..." %}</P>

<p><i><a href="{% url 'main_page' %}">View the main page without logging in.</a></i></p>

<script language="JavaScript">
    document.getElementById("id_username").focus();
</script>

</BODY></HTML>

Change not-logged-in redirect

In
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/view__profile.py
Change this:

PROFILE_LOGGED_OUT_REDIRECT_URL_NAME='main_page'
"""
TEMPORARY VALUE, only while the login view does not exist. Once it's
created, change this to 'login', and eliminate this comment. This
is also used by the tests.

The 'main_page' will be created in the next post. The 'login' page
will be created in the post after that.
"""

to this:

PROFILE_LOGGED_OUT_REDIRECT_URL_NAME='login'
"""This is also used by the tests."""

Add in the login and logout links

In both
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/templates/auth_lifecycle/user_profile.html
and
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/templates/auth_lifecycle/main_page.html

change “...{% trans "Logout" %}...” to

<a href="{% url 'logout_then_login' %}">{% trans "Logout" %}</a>

and “...{% trans "Login" %}...” to

<a href="{% url 'login' %}">{% trans "Login" %}</a>

URL configurations

Add the following line to the urlpatterns list in
    /home/myname/django_auth_lifecycle/djauth_root/django_auth_lifecycle/urls.py

url(r'^auth/', include('auth_lifecycle.registration.urls')),

This refers to the new authentication-specific URLconf, which should be saved as
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/urls.py

from django.conf.urls import patterns, url
urlpatterns = patterns('',
    url(r"^login/$", "django.contrib.auth.views.login", name="login"),
    url(r"^logout_then_login/$",
        "django.contrib.auth.views.logout_then_login",
        {"login_url": "login"} , name="logout_then_login"),
)

The tests

We will using django-webtest to test our login page. To install it:

  • source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate
  • sudo /home/myname/django_auth_lifecycle/djauth_venv/bin/pip install webtest django-webtest

Save the following as
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/test_login_basic.py

"""
Tests for authentication related views.

DEPENDS ON TEST:     test__utilities.py
DEPENDED ON BY TEST: None

To run the tests in this file:
    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.registration.test_login_basic

See the top of <link to .test__utilities> for more information.
"""
from auth_lifecycle.test__utilities import UserFactory, TEST_PASSWORD
from django.core.urlresolvers       import reverse
from django_webtest                 import WebTest
import time
import webtest.http

class AuthTest(WebTest):
    """Tests for authentication related views."""
    def setUp(self):
        self.user = UserFactory()

    def test_login(self):
        """
        Log a user in, which takes you to the main page. Because they're
        logged in, the main page contains a link to their profile page. Go
        to it. The profile page contains their email address and a link to
        logout. Click that link, which expires the session and takes you
        back to the login page.
        """

        #The following warning is given even before the test database is
        #created:
        #
        #/home/myname/django_files/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157:
        #DeprecationWarning: The value of convert_charrefs will become True in 3.5. You are encouraged to set the value explicitly.
        #
        #It is also given repeatedly during the test. I therefore believe
        #it is unrelated to our code.

        #Log a user in,
        form = self.app.get(reverse('login')).form
        form['username'] = self.user.username
        form['password'] = TEST_PASSWORD
        response_main_page = form.submit().follow()

        #which takes you to the main page.
        self.assertEqual(response_main_page.context['user'].username,
                         self.user.username)

        #Because they're logged in, the main page contains a link to their
        #profile page.
        user_prfl_url = reverse('user_profile')
        self.assertIn(user_prfl_url, str(response_main_page.content))

        #Go to it. The profile page contains their email address
        response_profile_page = response_main_page.click(href=user_prfl_url)
        assert self.user.email in response_profile_page

        #and a link to logout
        logout_url = reverse('logout_then_login')
        self.assertIn(logout_url, str(response_profile_page.content))

        #Click that link, which expires the session and takes you back to
        #the login page.
        response_login_page = response_profile_page.click(href=logout_url).follow()
        self.assertIn('value="login"', str(response_login_page.content))

        self.assertFalse(
            response_login_page.context['user'].is_authenticated())

Output

The deprecation warnings are unrelated to our test code, so we’re going to ignore them. Our test passed.

/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/imp.py:32: PendingDeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  PendingDeprecationWarning)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/importlib/_bootstrap.py:321: RemovedInDjango19Warning: django.utils.importlib will be removed in Django 1.9.
  return f(*args, **kwds)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/webtest/utils.py:10: DeprecationWarning: The value of convert_charrefs will become True in 3.5. You are encouraged to set the value explicitly.
  unescape_html = html_parser.HTMLParser().unescape
Creating test database for alias 'default'...
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The strict argument and mode are deprecated.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The value of convert_charrefs will become True in 3.5. You are encouraged to set the value explicitly.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The strict argument and mode are deprecated.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The value of convert_charrefs will become True in 3.5. You are encouraged to set the value explicitly.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The strict argument and mode are deprecated.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The value of convert_charrefs will become True in 3.5. You are encouraged to set the value explicitly.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The strict argument and mode are deprecated.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
/home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/bs4/builder/_htmlparser.py:157: DeprecationWarning: The value of convert_charrefs will become True in 3.5. You are encouraged to set the value explicitly.
  parser = BeautifulSoupHTMLParser(*args, **kwargs)
.
----------------------------------------------------------------------
Ran 1 test in 0.214s

OK
Destroying test database for alias 'default'...

Give it a try!

Follow these steps to start your server, and then login with your superuser account (admin/admin), or a new account that you create in your admin interface (http-colon-slash-slashmy.website/admin/).

In the next post, we’ll add a remember-me checkbox to the login page.

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

…to be continued…

(cue cliffhanger segue music)

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s