Monthly Archives: January 2015

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)

Useful git tips

Bash utility to trim whitespace from the ends of a string

Added 1/25/2015

#http://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-bash-variable#comment21953456_3232433
alias trim="sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*\$//g'"

Example use (also used by the below gitf Bash function):

param=$(echo "   Hello!" | trim)
echo "$param"                     #Outputs "Hello!"

Bash function to search for all commits containing a file that has *changed* (whose name matches a glob)

Added 1/24/2015

:<<COMMENT
	Searches all commits in the current git repository containing a file
	that has *changed*, whose name matches a glob. If the glob does not
	contain any asterisks, then it is surrounded by them on both sides.

	Usage:
		gitf "05"     #Equivalent to "*05*"
		gitf "05_*"

	Parameter is required, and must be at least one non-whitespace
	character.

	See:
	- http://stackoverflow.com/questions/28119379/bash-function-to-find-all-git-commits-in-which-a-file-whose-name-matches-a-rege/28120305
	- http://stackoverflow.com/questions/28094136/bash-function-to-search-git-repository-for-a-filename-that-matches-regex/28095750
	- http://stackoverflow.com/questions/372506/how-can-i-search-git-branches-for-a-file-or-directory/372654#372654

	The main "git log" line is based on this answer
	- http://stackoverflow.com/a/28119940/2736496
	by Stack Overflow user Greg Bacon
	- http://stackoverflow.com/users/123109/greg-bacon

	With thanks to SwankSwashbucklers
	- http://stackoverflow.com/users/2615252/swankswashbucklers

	Short description: Stored in GITF_DESC
COMMENT
#GITF_DESC: For "aliaf" command (with an 'f'). Must end with a newline.
GITF_DESC="gitf [glob]: Searches all commits in the current git repository containing a file	that has *changed*, whose name matches a glob.\n"
gitf()  {
	#Exit if no parameter is provided (if it's the empty string)
		param=$(echo "$1" | trim)
		echo "$param"
		if [ -z "$param" ]  #http://tldp.org/LDP/abs/html/comparison-ops.html
		then
		  echo "Required parameter missing. Cancelled"; return
		fi

	#http://stackoverflow.com/questions/229551/string-contains-in-bash/229606#229606
	if [[ $param != *"*"* ]]
	then
	  param="*$param*"
	fi

	echo "Searching for \"$param\"..."

	git log -p --name-only --oneline --diff-filter=AMD --branches --tags -- "$param"
}

Which commit is currently checked out?

[source]

$ git show --oneline -s
cb96da8 Changed Sublime project desktop/temps to desktop.

A better git log: git lol

[source]

Install with

git config --global --add alias.lol "log --graph --decorate --pretty=oneline --abbrev-commit --all"

Use:

$ git lol
* 9f0349d (HEAD, origin/master, origin/HEAD, master) Now finally does stinking pull first.
* 1cc98b0 Added auto-update.
* 405970a Initial import.

(An alternative: git log --oneline --graph --decorate --all)

I just broke Delphi Forums with a less-than sign

I posted my very exciting finding on the Tough Pigs fan forum, which is hosted by Delphi, with the subject

The Muppets > Toots Thieleman < Billy Joel

and it broke the entire forum.

I spent twenty minutes trying to figure out if it my browser’s script, ad, or tracker blockers were to blame. Then I tried deleting all the site’s cookies, thinking a search might have been stuck.

But no. It thinks the less-than sign is the beginning of an html tag, and the entire forum is down. That’s a little embarrassing for me, but even more so for the programmers/technical management at Delphi. I’m the first person in their thirty year history to use a less-than in a post title?

The mobile site also choked, but only for this post, not the site as a whole. (The forum administrator required assistance from Delphi in order to fix it.)

Muppets-Billy Joel connection: Toots Thieleman’s harmonica

Picking up the boys early from school tomorrow, and heading up to New York for Sesame Street 45-th anniversary exhibit at Lincoln Center, which is followed by the Tough Pigs Sesame Street Muppet Vault. Last year, we headed up with my oldest only, to see the premiere of Muppets Most Wanted.

Thanks to Retolder Kevin, I just learned that Toots Thieleman is the amazing harmonica player on both Billy Joel’s Leave A Tender Moment Alone and in the closing theme song for Sesame Street. That’s one of the biggest connections there are between these two amazing things: The Muppets and Billy Joel. Being a dual super-fan, this is a pretty huge piece of news.