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

In parts one, two, and three, we set up the model and trivial website, and in four, a basic, no-frills login page. Today, we’re going to add in “remember me” functionality, so a user’s session can optionally be extended to two weeks (when checked), instead of until browser close (when unchecked).

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

Warning: As best as I understand it (I would appreciate resources documenting this), the major browsers completely ignore this setting, and have designated the problem as low priority. So although this chapter may be pointless, it’s such basic functionality–and easy to implement–I’m choosing to keep it.

In the template
    /home/jeffy/django_files/djauth_lifecycle_tutorial/djauth_root/auth_lifecycle/templates/registration/login.html
add this line between the password field and the submit button:

<label><input name="remember" type="checkbox">Remember me</label>

In the authentication URL config
    /home/jeffy/django_files/djauth_lifecycle_tutorial/djauth_root/auth_lifecycle/authentication/urls.py
change the login line to:

url(r"^login/$",
    "auth_lifecycle.registration.view_login.login_maybe_remember",
    name="login"),

And, finally, save the following as
    /home/jeffy/django_files/djauth_lifecycle_tutorial/djauth_root/auth_lifecycle/authentication/view_login.py

"""
Renders authentication-specific views for the user-authentication-
lifecycle project.
"""
from django.contrib.auth.views import login

def login_maybe_remember(request, *args, **kwargs):
    """
    Login, with the addition of 'remember-me' functionality. If the
    remember-me checkbox is checked, the session is remembered for
    SESSION_COOKIE_AGE seconds. If unchecked, the session expires at
    browser close.

    - https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-SESSION_COOKIE_AGE
    - https://docs.djangoproject.com/en/1.7/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry
    - https://docs.djangoproject.com/en/1.7/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.get_expire_at_browser_close
    """
    if request.method == 'POST' and not request.POST.get('remember', None):
        #This is a login attempt and the checkbox is not checked.
        request.session.set_expiry(0)

    # print(request.session.get_expiry_age())
    # print(request.session.get_expire_at_browser_close())

    return login(request, *args, **kwargs)

Tests

This testing code is not working yet. I haven’t yet figured out how to test expiring cookies but, given the warning at the beginning of this tutorial-part, it’s low priority.

Save the following as

    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/test_login_remember_me.py

"""
Tests for the remember-me functionality on the login page.

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

To run the tests in this file:
    1. source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate
    2. cd /home/myname/django_auth_lifecycle/djauth_root/
    3. python -Wall manage.py test auth_lifecycle.registration.test_login_remember_me.py

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
from webtest                        import http
from webtest.debugapp               import debug_app
from webtest.http                   import StopableWSGIServer
import os
import time

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

    def test_login_dont_remember(self):
        """
        Log a user in with the remember-me checkbox unchecked. This takes
        you to the main page. Because they're logged in, the main page
        contains a link to their profile page. Restart the browser, and
        the session should be expired. Therefore, instead of a link to the
        profile, there should be a link to login.
        """

        #Log a user in with the remember-me checkbox unchecked.
        form = self.app.get(reverse('login')).form
        form['username'] = self.user.username
        form['password'] = TEST_PASSWORD
        form['remember'] = 'unchecked'
        response_main_page = form.submit().follow()

        #This 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.
        self.assertIn(reverse('user_profile'), str(response_main_page.content))

        #Restart the browser,
        http.StopableWSGIServer.shutdown()
        time.sleep(2)   #Two seconds
        http.StopableWSGIServer.run()

        #and the session should be expired.
        self.assertFalse(
            response_login_page.context['user'].is_authenticated())

        #Therefore, instead of a link to the profile, there should be a
        #link to login.
        response_main_page = self.app.get(reverse('main_page'))
        assert reverse('login') in response_main_page


    def test_login_dont_remember(self):
        """
        Log a user in with the remember-me checkbox checked. This takes
        you to the main page. Because they're logged in, the main page
        contains a link to their profile page. Restart the browser, and
        the session should still be active. Therefore, the link to their
        profile should still be there.
        """

        #Log a user in with the remember-me checkbox checked.
        form = self.app.get(reverse('login')).form
        form['username'] = self.user.username
        form['password'] = TEST_PASSWORD
        form['remember'] = 'checked'
        response_main_page = form.submit().follow()

        #This 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))

        #Restart the browser,
        http.StopableWSGIServer.shutdown()
        time.sleep(2)   #Two seconds
        http.StopableWSGIServer.run()

        #and the session should still be active.
        self.assertFalse(
            response_login_page.context['user'].is_authenticated())

        #Therefore, the link to their profile should still be there.
        response_main_page = self.app.get(reverse('main_page'))
        assert user_prfl_url in response_main_page

Output

...When I get the above code working...

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/). A reminder of the warning at the top of this post.

In the next post, we’ll add a JavaScript check (backed by Django) to prevent logins for obviously-bogus usernames and passwords, based on required min-max lengths.

[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