Monthly Archives: December 2014

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)

How I backup, from the point of view of (the incredible) XYplorer

Posted in XYplorer‘s user forum, under the topic “Other Software > How I backup”: http://www.xyplorer.com/xyfc/viewtopic.php?f=11&t=10614&p=115366#p115366

A month or so ago, I had my first true hard drive failure (terabyte! luckily recovered everything), followed a few days later by my five-year-old computer, which just wouldn’t turn on anymore…not even the initial BEEP. So much for the recovery disk.

I now have a four year contract with crashplan, which is only around four bucks a month. Once a day, or whenever I press a button, it backs up my computer to both an external hard drive and to their servers. It also backs up my wife’s laptop to MY computer…which is then backed up to those other two places. Pretty neat. All included in the four bucks. My wife’s only got a couple gigs of critical data, I’ve got several hundred gigs. Much of it music…including 38 gigs of Billy Joel *audio*, thank you very much :)

I also backup individual critical things to bitbucket, which is a Git repository, which is normally used for version control and branching of programming projects. For example, my XY configuration (C:\Users\aliteralmind\AppData\Roaming\XYplorer\) is a Git repository. Whenever I make any significant change, I save its configuration, shut down XY (not sure if shutting down is critical, but I’m nervous something might not be fully saved…), then in a shell/command prompt, I go to that directory and run either the following script with something like

git_add_commit_push_master.bat "Added custom toolbar button for custom layouts"

REM For use in all project sandboxes.
REM Save this in the project's root directory,
REM with the name:
REM    git_add_commit_push_master.bat

;set branch=%1
;set commit_msg=%2
set branch=master
set commit_msg=%1

REM ECHO The commit message is the one and only command-line parameter.
REM ECHO About to do
REM    THE FOLLOWING LINE SHOULD BE colon-upright slash!
REM    NOT an *EMOJI* <span>!!!
REM    DARN YOU WORDPRESS!!!
REM ECHO 1.  git add --all :/
REM ECHO 2.  git commit -m &quot;%commit_msg%&quot;
REM ECHO 3.  git push -u origin %branch%
REM PAUSE Press a key to proceed.

git add --all :/
REM PAUSE
git commit -m %commit_msg%
REM PAUSE
git push -u origin %branch%

Or more often just call the following script with no command-line parameters:

git_add_commit_push_master_quick.bat

call git_add_commit_push_master.bat &quot;Quick save (no message)&quot;

To quickly get to any repository’s directory I have an item in my XY catalog to copy it:

For example:

I am starting to use Listary in place of these catalog items (Although it’s annoying in Listary, to have to find the directory, then press the right arrow, then type “cpc” to copy-path-to-clipboard, and then enter to confirm. The author says the upcoming version will make this easier.)

Even though I likely don’t need versioning (and definitely don’t need branching!) for my XY configuration, it’s nice to know older versions are out there in case I backup something that’s truly messed up. Every now and then I just completely recreate the repository, to obliterate old versions (keeping track of every version, git repositories can balloon in size).

What’s beautiful about this is that everything is offline. Dropbox and mediafire and similar cloud tools are always online…always attached to your computer, so if you’re hit with something like crypto locker or some bad virus, then your dropbox–all of your backups!!–are immediately infected as well. For me this is an obvious dealbreaker, and I would be surprised if a “disconnect every time” version wasn’t eventually offered. With crashplan and bitbucket (and github, etc.), they’re backed up and then immediately disconnected, each time, which is so much safer.