Category Archives: Django

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

…This chapter is complete except no tests yet…


In parts one, two, and three, we set up the model and trivial website, and in four, five, and six, a working login page and logout links. In this post, we’re going to add a final feature to the login page: a password reset link, which sends a one-time-use-only link to the user’s email account. When clicked on, they are presented with a set-your-new-password form.

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

Reset your password: Overview

This is a time-consuming, although straight-forward feature to implement. By far the hardest thing is understanding the sequence of events, particularly because the views are confusingly named. This is how it works (with specifics based on the implementation in this chapter):

  1. On the login page there is an “I forgot my password” link.
  2. Click on it and you are taken to a page on which you need to enter your the email address from when you created the account. Pressing submit sends a one-time-only reset-my-password link to the user’s email. The view for this page is django.contrib.auth.views.password_reset.
  3. After it’s submitted, another page appears whose only purpose is to inform that the the email was sent, and they should go and view it for further instructions. The view for this page is django.contrib.auth.views.password_reset_done.
  4. The email itself is sent. The template for this email is specified by the email_template_name parameter of the password_reset view. See the next section for an example email.
  5. The link in the email takes you to the “set your new password” form, which includes a redundant confirmation field. The view for this page is django.contrib.auth.views.password_reset_confirm.
  6. After the form is submitted, the final page is presented, which only states “your password has been changed”, and likely provides a link back to the login page. The view for this page is django.contrib.auth.views.password_reset_complete.

Although I would choose these views to have clearer names more along the lines of

  • password_reset_1of4_email_request,
  • password_reset_2of4_email_sent,
  • pwd_reset_3of4_new_pwd_form, and
  • password_reset_4of4_finished

the existing names are here to stay. We are going to use the “better” ones as much as possible, though.

Set up email: Print to console only

Normally an email is actually sent (how to do this). Instead, we’re going to print its contents to the console. This is trivially-implemented by adding a single variable to
    /home/myname/django_auth_lifecycle/djauth_root/django_auth_lifecycle/settings.py

# https://docs.djangoproject.com/en/1.7/topics/email/#django.core.mail.backends.smtp.EmailBackend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

This is what the email looks like when printed to the console, with some empty lines removed (the 127.0.0.1.:8001 in the link must be changed to the name of your webserver):

MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: Password reset on 127.0.0.1:8001
From: webmaster@localhost
To: myemailaddress@yahoo.com
Date: Wed, 18 Feb 2015 16:38:02 -0000
Message-ID: 

You're receiving this email because you requested a password reset for your user account at 127.0.0.1:8001.
Please go to the following page and choose a new password:

http://127.0.0.1:8001/auth/pwd_reset_3of4_new_pwd_form/MQ/3zd-add9dfa05216b9ead4cc/

Your username, in case you've forgotten: admin
Thanks for using our site!
The 127.0.0.1:8001 team
-------------------------------------------------------------------------------

Set the login view name

One more change in the settings file.

In order for the final “your password was successfully changed” view (password_reset_complete) to link back to the login page, we must tell it where to link to, since ours is not using the default name. Add the LOGIN_URL variable:

# https://docs.djangoproject.com/en/1.7/ref/settings/#login-url
LOGIN_URL="login"     #View name in auth_lifecycle.registration.urls

Activate the forgot-my-password link

In
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/templates/registration/login.html

Change

...I forgot my password...

to

<a href="{% url 'password_reset' %}">I forgot my password</a>

Custom templates: Overview

Aside from the email itself, each of the four views has its own template. Creating custom versions of these templates is optional. You could use all of the built-in defaults and skip straight to updating urls.py. The directory containing all default templates, as installed with Django, is:
    /home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/django/contrib/admin/templates/registration/

To create a custom template, you can

  • Duplicate the entire custom template and tweak what you like, or
  • Extend the template and override only the needed sections

Regardless which way you choose, these default templates should be used as a reference if you encounter any problems.

The relative path of the file (as based off of one of the TEMPLATE_DIRS) must be either

  • Equal to the default value of the view’s template_name parameter, as specified in each view’s documentation. For example, the password_reset view states “Defaults to registration/password_change_done.html if not supplied.”
  • Or set to an alternate value, by passing it through it’s url-entry:
    url(r"^pwd_reset_3of4_new_pwd_form/(?P<uidb64>\w+)/(?P<token>[\w-]+)/$",
        "django.contrib.auth.views.password_reset_confirm",
        { "template_name": "registration/pwd_reset_3of4_new_pwd_form.html" }

(The email itself is specified by the email_template_name parameter in password_reset.)

Custom template: pwd_reset_3of4_new_pwd_form

The only custom template we’ll be creating is for the set-your-new-password form (django.contrib.auth.views.password_reset_done), so we can also do a client-side check for the password lengths, and that they’re equal. This will be implemented with JQuery Validation.

This is the default template, as installed by Django:
    /home/myname/django_auth_lifecycle/djauth_venv/lib/python3.4/site-packages/django/contrib/admin/templates/registration/password_reset_confirm.html

{% extends "admin/base_site.html" %}
{% load i18n %}

{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; {% trans 'Password reset confirmation' %}
</div>
{% endblock %}

{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}

{% if validlink %}

<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>

<form action="" method="post">{% csrf_token %}
{{ form.new_password1.errors }}
<p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p>
{{ form.new_password2.errors }}
<p class="aligned wide"><label for="id_new_password2">{% trans 'Confirm password:' %}</label>{{ form.new_password2 }}</p>
<p><input type="submit" value="{% trans 'Change my password' %}" /></p>
</form>

{% else %}

<p>{% trans "The password reset link was invalid, possibly because it has already been used.  Please request a new password reset." %}</p>

{% endif %}

{% endblock %}

The entire “content” block must be replaced with our custom code. The form as associated to the JavaScript by giving it the name “newPwdForm“.

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

{% extends "registration/password_reset_confirm.html" %}
{% comment %}
   The extends Must be the first line in the template. This comment may
   not be before it.

   Example use:
   - http://stackoverflow.com/a/28570678/2736496
   Documentation:
   - https://docs.djangoproject.com/en/1.7/ref/templates/builtins/#std:templatetag-extends
{% endcomment %}
{% load i18n %}       {# For the "trans" tag #}
{% block content %}

{% if validlink %}

<p>{% trans "Please enter your new password twice so we can verify." %}</p>
<p>{% trans "you typed it in correctly." %}</p>

<form action="" method="post" id="newPwdForm"><!-- Form id rqd by JS. -->
   {% csrf_token %}
   {{ form.new_password1.errors }}
   <p class="aligned wide">
      <label for="id_new_password1">{% trans 'New password:' %}</label>
      {{ form.new_password1 }}</p>
   {{ form.new_password2.errors }}
   <p class="aligned wide">
      <label for="id_new_password2">{% trans 'Confirm password:' %}</label>
      {{ form.new_password2 }}</p>
   <p><input type="submit" value="{% trans 'Change my password' %}" /></p>
</form>

{% else %}

<p>{% trans "The password reset link was invalid, possibly because it" %}
{% trans "has already been used.  Please request a new password reset." %}</p>

{% endif %}

<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://jqueryvalidation.org/files/dist/jquery.validate.min.js"></script>
<script src="http://jqueryvalidation.org/files/dist/additional-methods.min.js"></script>
<script>
   //These values come from auth_lifecycle.models
   var minPassLen = {{ PASSWORD_MIN_LEN }}; //PASSWORD_MIN_LEN
   var maxPassLen = {{ PASSWORD_MAX_LEN }}; //PASSWORD_MAX_LEN

   var passwordMsg = "{% trans "Password must be between " %}" + minPassLen +
      "{% trans " and " %}" + maxPassLen +
      "{% trans " characters, inclusive." %}";

   jQuery.validator.setDefaults({
      success: "valid",
      //Avoids form submit. Comment when in production...START
      //debug: true
      //submitHandler: function() {
      //   alert("Success! The form was pretend-submitted!");
      //}
      //Avoids form submit. Comment when in production...END
   });
   $( "#newPwdForm" ).validate({
      rules: {
         new_password1: {
            required: true,
            minlength: minPassLen,
            maxlength: maxPassLen
         },
         new_password2: {
            //http://jqueryvalidation.org/equalTo-method
            equalTo: "#id_new_password1"
         }
      },
      messages:  {
         new_password1: {
            required: "{% trans "Password required" %}",
            minlength: passwordMsg,
            maxlength: passwordMsg
         }
      }
   });
</script>

{% endblock %}

Server-side check

As done with the login form, we’re going to update the set-a-new-password form to enforce length. The default form used by this view, django.contrib.auth.forms.SetPasswordForm, does check for the passwords being equal, but does not have any min or max lengths:

new_password1 = forms.CharField(label=_("New password"),
                                widget=forms.PasswordInput)

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

from auth_lifecycle.models     import PASSWORD_MIN_LEN, PASSWORD_MAX_LEN
from auth_lifecycle.registration.view_login import get_min_max_incl_err_msg
from django                    import forms    #NOT django.contrib.auth.forms
from django.contrib.auth.forms import SetPasswordForm
from django.utils.translation  import ugettext, ugettext_lazy as _

min_max_len_err_msg = get_min_max_incl_err_msg(PASSWORD_MIN_LEN, PASSWORD_MAX_LEN)

class SetPasswordFormEnforceLength(SetPasswordForm):
    """
    A `SetPasswordForm` that enforces min/max lengths.
    - https://docs.djangoproject.com/en/1.7/_modules/django/contrib/auth/forms/#SetPasswordForm

    Pass this into the login form via the `set_password_form` parameter.
    - https://docs.djangoproject.com/en/1.7/topics/auth/default/#django.contrib.auth.views.password_reset_confirm
    Which is done in `registration/urls.py`.
    """
    new_password1 = forms.CharField(label=_("New password"),
                                    widget=forms.PasswordInput,
                                    min_length=PASSWORD_MIN_LEN,
                                    max_length=PASSWORD_MAX_LEN,
                                    error_messages={
                                        'min_length': min_max_len_err_msg,
                                        'max_length': min_max_len_err_msg })

(To repeat the warning from the top of part six: The server-side length checks are “succeeding” but crashing–that is, they only crash when the lengths are incorrect. While this is a critical problem, it only applies when the client-side JavaScript is disabled. Here is a Stack Overflow question documenting the problem. A solution would be greatly appreciated.)

Configure the urls

Replace the contents of
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/urls.py
with

from auth_lifecycle.models import PASSWORD_MIN_LEN, PASSWORD_MAX_LEN
from auth_lifecycle.registration.view_login import AuthenticationFormEnforceLength
from auth_lifecycle.registration.form_reset_set_new_pwd import SetPasswordFormEnforceLength
from django.conf.urls      import patterns, url
#Passing keyword arguments through url entries:
# - https://docs.djangoproject.com/en/1.7/topics/http/urls/#passing-extra-options-to-view-functions
urlpatterns = patterns('',
    url(r"^login/$",
        "auth_lifecycle.registration.view_login.login_maybe_remember",
        { "authentication_form": AuthenticationFormEnforceLength },
        name="login"),
    url(r"^logout_then_login/$", "django.contrib.auth.views.logout_then_login",
        {"login_url": "login"}, name="logout_then_login"),
    url(r"^password_reset_1of4_email_request/$",
        "django.contrib.auth.views.password_reset", name="password_reset"),
    url(r"^password_reset_2of4_email_sent/$",
        "django.contrib.auth.views.password_reset_done",
        name="password_reset_done"),
    url(r"^pwd_reset_3of4_new_pwd_form/(?P<uidb64>\w+)/(?P<token>[\w-]+)/$",
        "django.contrib.auth.views.password_reset_confirm",
        { "template_name": "registration/pwd_reset_3of4_new_pwd_form.html",
          "extra_context": { "PASSWORD_MIN_LEN": PASSWORD_MIN_LEN,
                             "PASSWORD_MAX_LEN": PASSWORD_MAX_LEN },
          "set_password_form": SetPasswordFormEnforceLength },
        name="password_reset_confirm"),
    #If NOT using a custom template:
    # url(r"^pwd_reset_3of4_new_pwd_form/(?P<uidb64>\w+)/(?P<token>[\w-]+)/$",
    #     "django.contrib.auth.views.password_reset_confirm",
    #     name="password_reset_confirm"),
    url(r"^password_reset_4of4_finished/$",
        "django.contrib.auth.views.password_reset_complete",
        name="password_reset_complete" ),
)

Tests

Save the following as

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

...

Output:


Our tests passed.

Give it a try!

Follow these steps to start your server. A reminder to check your console for the text of email.

In the next post, we move on to the change your password form. After that, the final steps are creating and deleting an account.

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

…to be continued…

(cue cliffhanger segue music)

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

In parts one, two, and three, we set up the model and trivial website, and in four and five, a login page with remember-me functionality. In this post, we’re going to upgrade it further, with a basic client-side (JavaScript) check to prevent logins from ever reaching the server when the username or password have an obviously-incorrect length. These min-max values will originate in the model code, and will be passed to the template by the view.

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

Two warnings:

  • As stated at the top of part one, I am unable to test these JavaScript features with Selenium, due to limitations in my server; it’s command-line only–non-GUI.
  • The server-side length checks are “succeeding” but crashing–that is, they only crash when the lengths are incorrect. While this is a critical problem, it only applies when the client-side JavaScript is disabled. Here is a Stack Overflow question documenting the problem. A solution would be greatly appreciated.

This post is the first to contain a substantial amount of JavaScript. It is therefore time to create a public static web server directory, into which the JavaScript will be placed.

  • The Nginx config file for this project is /etc/nginx/sites-available/django_auth_lifecycle
  • The paths to the Django-root and Virtualenv directories:
    • /home/myname/django_auth_lifecycle/djauth_root/
    • /home/myname/django_auth_lifecycle/djauth_venv/

Update the models

Were going to add four configuration variables that will be used by both the Django code and in the templates, that define the minimum and maximum lengths for both the username and password. Although the model itself will remain the same, models.py is an appropriate place to have these configuration variables.

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, and defines the absolute minimum and maximum
allowable username and password lengths.

The extra field: 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

USERNAME_MIN_LEN = 5
"""
The database allows one-character usernames. We're going to forbid
anything less than five characters.
"""
USERNAME_MAX_LEN = User._meta.get_field('username').max_length
"""
The maximum allowable username length, as determined by the database
column. Equal to
    `User._meta.get_field('username').max_length`
"""
PASSWORD_MIN_LEN = 5
"""
The password is stored in the database, not as plain text, but as it's
generated hash. Length is therefore not enforced by the database at all.
We're going to minimally protect users against themselves and impose an
five character minimum. (For real, I'd make this eight. When testing, I
make the password equal to the username, so it's temporarily shorter.)
"""
PASSWORD_MAX_LEN = 4096
"""
Imposing a maximum password length is not recommended:
- http://stackoverflow.com/questions/98768/should-i-impose-a-maximum-length-on-passwords

However, Django prevents an attack vector by forbidding excessively-long
passwords (See "Issue: denial-of-service via large passwords"):
- https://www.djangoproject.com/weblog/2013/sep/15/security/
"""

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

Update the view

The login view created in the previous chapter now needs to be updated so it pass those configuration variables to the template. These variables will be used by the client-side JavaScript. (An alternative to passing extra context to the template is via urls.py, as demonstrated in the next chapter.)

However, since we have no control over our users’ machines–and therefore cannot know if JavaScript is even enabled–it is also necessary to do this check on the server. The default form used by this view, django.contrib.auth.forms.AuthenticationForm, needs to be updated with our min/max lengths. Here are the original fields:

username = forms.CharField(max_length=254)
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)

Replace the contents of
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/view_login.py
with:

from auth_lifecycle.models     import PASSWORD_MIN_LEN, PASSWORD_MAX_LEN
from auth_lifecycle.models     import USERNAME_MIN_LEN, USERNAME_MAX_LEN
from django                    import forms    #NOT django.contrib.auth.forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.views import login
#from django.core.exceptions    import ValidationError
from django.utils.translation  import ugettext, ugettext_lazy as _

def login_maybe_remember(request, *args, **kwargs):
    """
    Login with remember-me functionality and length checking. 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)

    context = {}
    context["USERNAME_MIN_LEN"] = USERNAME_MIN_LEN
    context["USERNAME_MAX_LEN"] = USERNAME_MAX_LEN
    context["PASSWORD_MIN_LEN"] = PASSWORD_MIN_LEN
    context["PASSWORD_MAX_LEN"] = PASSWORD_MAX_LEN
    kwargs["extra_context"] = context

    print("authentication_form=" + str(kwargs["authentication_form"]));

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

def get_min_max_incl_err_msg(min_int, max_int):
    """A basic error message for inclusive string length."""
    "Must be between " + str(min_int) + " and " + str(max_int) + " characters, inclusive."

username_min_max_len_err_msg = get_min_max_incl_err_msg(USERNAME_MIN_LEN, USERNAME_MAX_LEN)
pwd_min_max_len_err_msg = get_min_max_incl_err_msg(PASSWORD_MIN_LEN, PASSWORD_MAX_LEN)

class AuthenticationFormEnforceLength(AuthenticationForm):
    """
    An `AuthenticationForm` that enforces min/max lengths.
    - https://docs.djangoproject.com/en/1.7/_modules/django/contrib/auth/forms/#AuthenticationForm

    Pass this into the login form via the `authentication_form` parameter.
    - https://docs.djangoproject.com/en/1.7/topics/auth/default/#django.contrib.auth.views.login
    Which is done in `registration/urls.py`.
    """
    username = forms.CharField(min_length=USERNAME_MIN_LEN,
                               max_length=USERNAME_MAX_LEN,
                               error_messages={
                                   'min_length': username_min_max_len_err_msg,
                                   'max_length': username_min_max_len_err_msg })
    password = forms.CharField(label=_("Password"), widget=forms.PasswordInput,
                                    min_length=PASSWORD_MIN_LEN,
                                    max_length=PASSWORD_MAX_LEN,
                                    error_messages={
                                        'min_length': pwd_min_max_len_err_msg,
                                        'max_length': pwd_min_max_len_err_msg })
#    def clean(self):
#        raise ValidationError("Yikes")

(A reminder of the second warning at the top of this chapter…)

The bounds in this form object cause maxlength attributes to be placed directly onto the html form elements:

<input id="id_username" maxlength="30" name="username" type="text" /></p>
<input id="id_password" maxlength="4096" name="password" type="password" /></p>

While this now makes the maximum portion of our JavaScript checks redundant, we’re going to leave them in anyway.

Update the template

Now to use these variables in the template.

Replace the contents of
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/templates/registration/login.html
with

{% load i18n %}       {# For the "trans" tag #}
{% load staticfiles %}{# References the static directory.             #}
<!DOCTYPE html>       {# Use with "{% static 'color_ajax_like.js' %}" #}
<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>

<form method="post" id="loginForm" action="{% url 'login' %}">
{% csrf_token %}
{{ form.as_p }}

   <label><input name="remember" type="checkbox">{% trans "Remember me" %}</label>

   <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">
   /*
      Before our JavaScript can be imported, the following variables need
      to be set from some Django variables. While these values could be
      hard-coded here, into the JavaScript, this allows the configuration
      to be centrally located.

      These four values come from auth_lifecycle.models and are required
      by validate_login_user_pass.js.
    */
   var minUserLen = {{ USERNAME_MIN_LEN }}; //USERNAME_MIN_LEN
   var maxUserLen = {{ USERNAME_MAX_LEN }}; //USERNAME_MAX_LEN
   var minPassLen = {{ PASSWORD_MIN_LEN }}; //PASSWORD_MIN_LEN
   var maxPassLen = {{ PASSWORD_MAX_LEN }}; //PASSWORD_MAX_LEN

   document.getElementById("id_username").focus();
</script>
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.13.0/jquery.validate.min.js"></script>
<script type='text/javascript' src="{% static 'js/validate_login_user_pass.js' %}"></script>

</BODY></HTML>

JavaScript validation code

We’ll be using the JQuery Validation Plugin. Here is a separate post about the plugin, where I demonstrate a basic use–the concepts of which are used here.

Save the following as
    /home/myname/django_auth_lifecycle/djauth_root/static/js/validate_login_user_pass.js
(create the "js" sub-directory).

//Configuration...START
   //The following variables are required to exist before this file is
   //loaded: minUserLen, maxUserLen, minPassLen, maxPassLen
   //
   //In addition, the id of the login <form> must be "loginForm"

   var usernameMsg = "Username must be between " + minUserLen + " and " +
                     maxUserLen + " characters, inclusive.";
   var passwordMsg = "Password must be between " + minPassLen + " and " +
                     maxPassLen + " characters, inclusive.";
   jQuery.validator.setDefaults({
      success: "valid",
      //Avoids form submit. Comment when in production...START
      // debug: true,
      // submitHandler: function() {
      //    alert("Success! The form was pretend-submitted!");
      // }
      //Avoids form submit. Comment when in production...END
   });
//Configuration...END
var validateLoginForm = function()  {
   // validate signup form on keyup and submit
   var config = {
      rules: {
         username: {
            required: true,
            minlength: minUserLen,
            maxlength: maxUserLen
         },
         password: {
            required: true,
            minlength: minPassLen,
            maxlength: maxPassLen
         },
      },
      messages: {
         username: {
            required: "Username required",
            minlength: usernameMsg,
            maxlength: usernameMsg
         },
         password: {
            required: "Password required",
            minlength: passwordMsg,
            maxlength: passwordMsg
         }
      }
   };
   $("#loginForm").validate(config);
}
$(document).ready(validateLoginForm);

Tests: Not much to do

Webtest directly accesses the WSGI application server, and therefore cannot test client-side JavaScript. The only thing we’re going to look for is that the variables actually made it into the template, and are being assigned to JavaScript variables as required by the above code.

First, we need a new simple utility testing function. Add this import statement to the top of
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/test__utilities.py

import re

and this function at the bottom:

def assert_pattern_in_string(test_instance, pattern, to_search):
    """
    Asserts that a pattern is found somewhere in a string. This calls

        `test_instance.assertIsNotNone(re.search(pattern, to_search))`

    A failure results in the full pattern and to-search strings being
    printed.
    """
    match = re.search(pattern, to_search)
    test_instance.assertIsNotNone(match,
        "pattern=" + pattern + ", to_search=" + to_search)

Now to create the test-proper. Save the following as
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/registration/test_login_user_pass_len_vars.py

"""
Tests to confirm that the username and password min/max length variables
are correctly passed through from the view, and assigned as expected.
django-webtest cannot test client-side JavaScript, because it only runs
through the WSGI application server.
- http://stackoverflow.com/a/12552319/2736496
- http://webtest.readthedocs.org/en/latest/#what-this-does

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_user_pass_len_vars

See the top of <link to .test__utilities> for more information.
"""
from auth_lifecycle.test__utilities         import assert_pattern_in_string
from django.test              import TestCase
from auth_lifecycle.models    import USERNAME_MIN_LEN, USERNAME_MAX_LEN
from auth_lifecycle.models    import PASSWORD_MIN_LEN, PASSWORD_MAX_LEN
from django.core.urlresolvers import reverse

class UserPassLenVarsTestCase(TestCase):
    """
    Tests to confirm that the username and password min/max length
    variables are correctly passed through from the view, and assigned as
    expected.
    """

    def test_all_config_vars_passed(self):
        """
        Tests to confirm that the username and password min/max length
        variables are correctly passed through from the view, and assigned
        as expected.
        """
        self.client.logout() #Don't really have to, but why not?

        content_str = str(self.client.get(reverse('login')).content)

        assert_pattern_in_string(self,
            r"\bminUserLen += +" + str(USERNAME_MIN_LEN) + ";", content_str)
        assert_pattern_in_string(self,
            r"\bmaxUserLen += +" + str(USERNAME_MAX_LEN) + ";", content_str)
        assert_pattern_in_string(self,
            r"\bminPassLen += +" + str(PASSWORD_MIN_LEN) + ";", content_str)
        assert_pattern_in_string(self,
            r"\bmaxPassLen += +" + str(PASSWORD_MAX_LEN) + ";", content_str)

Output:

/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)
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.039s

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

Our tests passed.

Give it a try!

Follow these steps to start your server, and try to login with no username and/or password, or one that is too short.

In the next post, we’ll complete our login page functionality with a forgot-my-password link. After that, it’s onto the change your password form, and finally, creating and deleting an account.

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

…to be continued…

(cue cliffhanger segue music)

Tip for debugging Django templates

Put this anywhere on your page:

<pre>{% filter force_escape %}{% debug %}{% endfilter %}</pre>
{#       //Move THIS open-comment line above the debug line to suppress.
         //Below to activate.
#}

Example output:

DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10,
                            'ERROR': 40,
                            'INFO': 20,
                            'SUCCESS': 25,
                            'WARNING': 30},
 'LANGUAGES': (('af', 'Afrikaans'),
               ('ar', 'Arabic'),
               ('ast', 'Asturian'),
               ('az', 'Azerbaijani'),
               ('bg', 'Bulgarian'),
               ('be', 'Belarusian'),
               ('bn', 'Bengali'),
               ('br', 'Breton'),
               ('bs', 'Bosnian'),
               ('ca', 'Catalan'),
               ('cs', 'Czech'),
               ('cy', 'Welsh'),
               ('da', 'Danish'),
               ('de', 'German'),
               ('el', 'Greek'),
               ('en', 'English'),
               ('en-au', 'Australian English'),
               ('en-gb', 'British English'),
               ('eo', 'Esperanto'),
               ('es', 'Spanish'),
               ('es-ar', 'Argentinian Spanish'),
               ('es-mx', 'Mexican Spanish'),
               ('es-ni', 'Nicaraguan Spanish'),
               ('es-ve', 'Venezuelan Spanish'),
               ('et', 'Estonian'),
               ('eu', 'Basque'),
               ('fa', 'Persian'),
               ('fi', 'Finnish'),
               ('fr', 'French'),
               ('fy', 'Frisian'),
               ('ga', 'Irish'),
               ('gl', 'Galician'),
               ('he', 'Hebrew'),
               ('hi', 'Hindi'),
               ('hr', 'Croatian'),
               ('hu', 'Hungarian'),
               ('ia', 'Interlingua'),
               ('id', 'Indonesian'),
               ('io', 'Ido'),
               ('is', 'Icelandic'),
               ('it', 'Italian'),
               ('ja', 'Japanese'),
               ('ka', 'Georgian'),
               ('kk', 'Kazakh'),
               ('km', 'Khmer'),
               ('kn', 'Kannada'),
               ('ko', 'Korean'),
               ('lb', 'Luxembourgish'),
               ('lt', 'Lithuanian'),
               ('lv', 'Latvian'),
               ('mk', 'Macedonian'),
               ('ml', 'Malayalam'),
               ('mn', 'Mongolian'),
               ('mr', 'Marathi'),
               ('my', 'Burmese'),
               ('nb', 'Norwegian Bokmal'),
               ('ne', 'Nepali'),
               ('nl', 'Dutch'),
               ('nn', 'Norwegian Nynorsk'),
               ('os', 'Ossetic'),
               ('pa', 'Punjabi'),
               ('pl', 'Polish'),
               ('pt', 'Portuguese'),
               ('pt-br', 'Brazilian Portuguese'),
               ('ro', 'Romanian'),
               ('ru', 'Russian'),
               ('sk', 'Slovak'),
               ('sl', 'Slovenian'),
               ('sq', 'Albanian'),
               ('sr', 'Serbian'),
               ('sr-latn', 'Serbian Latin'),
               ('sv', 'Swedish'),
               ('sw', 'Swahili'),
               ('ta', 'Tamil'),
               ('te', 'Telugu'),
               ('th', 'Thai'),
               ('tr', 'Turkish'),
               ('tt', 'Tatar'),
               ('udm', 'Udmurt'),
               ('uk', 'Ukrainian'),
               ('ur', 'Urdu'),
               ('vi', 'Vietnamese'),
               ('zh-cn', 'Simplified Chinese'),
               ('zh-hans', 'Simplified Chinese'),
               ('zh-hant', 'Traditional Chinese'),
               ('zh-tw', 'Traditional Chinese')),
 'LANGUAGE_BIDI': False,
 'LANGUAGE_CODE': 'en-us',
 'MEDIA_URL': '',
 'STATIC_URL': '/static/',
 'TIME_ZONE': 'UTC',
 'csrf_token': <django.utils.functional.lazy..__proxy__ object at 0xb5dc016c>,
 'messages': ,
 'perms': ,
 'user': <SimpleLazyObject: <function AuthenticationMiddleware.process_request.. at 0xb6766df4>>}{'form': ,
 'next': '/auth_lifecycle/',
 'site': ,
 'site_name': '127.0.0.1:8001'}{'False': False, 'None': None, 'True': True}

{'__future__': <module '__future__' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/__future__.py'>,
 '__main__': <module '__main__' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/bin/gunicorn'>,
 '_ast': <module '_ast' (built-in)>,
 '_bisect': <module '_bisect' (built-in)>,
 '_bootlocale': <module '_bootlocale' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/_bootlocale.py'>,
 '_bz2': <module '_bz2' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/lib-dynload/_bz2.cpython-34m-i386-linux-gnu.so'>,
 '_codecs': <module '_codecs' (built-in)>,
 '_collections': <module '_collections' (built-in)>,
 '_collections_abc': <module '_collections_abc' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/_collections_abc.py'>,
 '_compat_pickle': <module '_compat_pickle' from '/usr/lib/python3.4/_compat_pickle.py'>,
 '_datetime': <module '_datetime' (built-in)>,
 '_decimal': <module '_decimal' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/lib-dynload/_decimal.cpython-34m-i386-linux-gnu.so'>,
 '_frozen_importlib': <module 'importlib._bootstrap' (frozen)>,
 '_functools': <module '_functools' (built-in)>,
 '_hashlib': <module '_hashlib' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/lib-dynload/_hashlib.cpython-34m-i386-linux-gnu.so'>,
 '_heapq': <module '_heapq' (built-in)>,
 '_imp': <module '_imp' (built-in)>,
 '_io': <module 'io' (built-in)>,
 '_json': <module '_json' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/lib-dynload/_json.cpython-34m-i386-linux-gnu.so'>,
 '_locale': <module '_locale' (built-in)>,
 '_lzma': <module '_lzma' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/lib-dynload/_lzma.cpython-34m-i386-linux-gnu.so'>,
 '_markupbase': <module '_markupbase' from '/usr/lib/python3.4/_markupbase.py'>,
 '_opcode': <module '_opcode' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/lib-dynload/_opcode.cpython-34m-i386-linux-gnu.so'>,
 '_operator': <module '_operator' (built-in)>,
 '_pickle': <module '_pickle' (built-in)>,
 '_posixsubprocess': <module '_posixsubprocess' (built-in)>,

...

 'psycopg2._json': <module 'psycopg2._json' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/site-packages/psycopg2/_json.py'>,
 'psycopg2._psycopg': <module 'psycopg2._psycopg' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/site-packages/psycopg2/_psycopg.cpython-34m.so'>,
 'psycopg2._range': <module 'psycopg2._range' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/site-packages/psycopg2/_range.py'>,
 'psycopg2.extensions': <module 'psycopg2.extensions' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/site-packages/psycopg2/extensions.py'>,
 'psycopg2.tz': <module 'psycopg2.tz' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/site-packages/psycopg2/tz.py'>,
 'pwd': <module 'pwd' (built-in)>,
 'queue': <module 'queue' from '/usr/lib/python3.4/queue.py'>,
 'quopri': <module 'quopri' from '/usr/lib/python3.4/quopri.py'>,
 'random': <module 'random' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/random.py'>,
 're': <module 're' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/re.py'>,
 'reprlib': <module 'reprlib' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/reprlib.py'>,
 'resource': <module 'resource' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/lib-dynload/resource.cpython-34m-i386-linux-gnu.so'>,
 'select': <module 'select' (built-in)>,
 'selectors': <module 'selectors' from '/usr/lib/python3.4/selectors.py'>,
 'shutil': <module 'shutil' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/shutil.py'>,
 'signal': <module 'signal' (built-in)>,
 'site': <module 'site' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/site.py'>,
 'sitecustomize': <module 'sitecustomize' from '/usr/lib/python3.4/sitecustomize.py'>,
 'socket': <module 'socket' from '/usr/lib/python3.4/socket.py'>,
 'socketserver': <module 'socketserver' from '/usr/lib/python3.4/socketserver.py'>,
 'sre_compile': <module 'sre_compile' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/sre_compile.py'>,
 'sre_constants': <module 'sre_constants' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/sre_constants.py'>,
 'sre_parse': <module 'sre_parse' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/sre_parse.py'>,
 'ssl': <module 'ssl' from '/usr/lib/python3.4/ssl.py'>,
 'stat': <module 'stat' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/stat.py'>,
 'string': <module 'string' from '/usr/lib/python3.4/string.py'>,
 'stringprep': <module 'stringprep' from '/usr/lib/python3.4/stringprep.py'>,
 'struct': <module 'struct' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/struct.py'>,
 'subprocess': <module 'subprocess' from '/usr/lib/python3.4/subprocess.py'>,
 'symbol': <module 'symbol' from '/usr/lib/python3.4/symbol.py'>,
 'sys': <module 'sys' (built-in)>,
 'sysconfig': <module 'sysconfig' from '/usr/lib/python3.4/sysconfig.py'>,
 'tarfile': <module 'tarfile' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/tarfile.py'>,
 'tempfile': <module 'tempfile' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/tempfile.py'>,
 'textwrap': <module 'textwrap' from '/usr/lib/python3.4/textwrap.py'>,
 'threading': <module 'threading' from '/usr/lib/python3.4/threading.py'>,
 'time': <module 'time' (built-in)>,
 'token': <module 'token' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/token.py'>,
 'tokenize': <module 'tokenize' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/tokenize.py'>,
 'traceback': <module 'traceback' from '/usr/lib/python3.4/traceback.py'>,
 'types': <module 'types' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/types.py'>,
 'unicodedata': <module 'unicodedata' (built-in)>,
 'unittest': <module 'unittest' from '/usr/lib/python3.4/unittest/__init__.py'>,
 'unittest.case': <module 'unittest.case' from '/usr/lib/python3.4/unittest/case.py'>,
 'unittest.loader': <module 'unittest.loader' from '/usr/lib/python3.4/unittest/loader.py'>,
 'unittest.main': <module 'unittest.main' from '/usr/lib/python3.4/unittest/main.py'>,
 'unittest.result': <module 'unittest.result' from '/usr/lib/python3.4/unittest/result.py'>,
 'unittest.runner': <module 'unittest.runner' from '/usr/lib/python3.4/unittest/runner.py'>,
 'unittest.signals': <module 'unittest.signals' from '/usr/lib/python3.4/unittest/signals.py'>,
 'unittest.suite': <module 'unittest.suite' from '/usr/lib/python3.4/unittest/suite.py'>,
 'unittest.util': <module 'unittest.util' from '/usr/lib/python3.4/unittest/util.py'>,
 'urllib': <module 'urllib' from '/usr/lib/python3.4/urllib/__init__.py'>,
 'urllib.error': <module 'urllib.error' from '/usr/lib/python3.4/urllib/error.py'>,
 'urllib.parse': <module 'urllib.parse' from '/usr/lib/python3.4/urllib/parse.py'>,
 'urllib.request': <module 'urllib.request' from '/usr/lib/python3.4/urllib/request.py'>,
 'urllib.response': <module 'urllib.response' from '/usr/lib/python3.4/urllib/response.py'>,
 'uu': <module 'uu' from '/usr/lib/python3.4/uu.py'>,
 'warnings': <module 'warnings' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/warnings.py'>,
 'weakref': <module 'weakref' from '/home/jeffy/django_files/djauth_lifecycle_tutorial/part_06/dalt06_venv/lib/python3.4/weakref.py'>,
 'wsgiref': <module 'wsgiref' from '/usr/lib/python3.4/wsgiref/__init__.py'>,
 'wsgiref.handlers': <module 'wsgiref.handlers' from '/usr/lib/python3.4/wsgiref/handlers.py'>,
 'wsgiref.headers': <module 'wsgiref.headers' from '/usr/lib/python3.4/wsgiref/headers.py'>,
 'wsgiref.simple_server': <module 'wsgiref.simple_server' from '/usr/lib/python3.4/wsgiref/simple_server.py'>,
 'wsgiref.util': <module 'wsgiref.util' from '/usr/lib/python3.4/wsgiref/util.py'>,
 'xml': <module 'xml' from '/usr/lib/python3.4/xml/__init__.py'>,
 'xml.dom': <module 'xml.dom' from '/usr/lib/python3.4/xml/dom/__init__.py'>,
 'xml.dom.NodeFilter': <module 'xml.dom.NodeFilter' from '/usr/lib/python3.4/xml/dom/NodeFilter.py'>,
 'xml.dom.domreg': <module 'xml.dom.domreg' from '/usr/lib/python3.4/xml/dom/domreg.py'>,
 'xml.dom.minicompat': <module 'xml.dom.minicompat' from '/usr/lib/python3.4/xml/dom/minicompat.py'>,
 'xml.dom.minidom': <module 'xml.dom.minidom' from '/usr/lib/python3.4/xml/dom/minidom.py'>,
 'xml.dom.xmlbuilder': <module 'xml.dom.xmlbuilder' from '/usr/lib/python3.4/xml/dom/xmlbuilder.py'>,
 'zipfile': <module 'zipfile' from '/usr/lib/python3.4/zipfile.py'>,
 'zipimport': <module 'zipimport' (built-in)>,
 'zlib': <module 'zlib' (built-in)>}

Configuring Nginx with a public static directory (for JavaScript, images, etc.)

This post describes how I configure a public static directory for a Django project, with Nginx (on Ubuntu) as the web server. This is where public, non-secure JavaScript, images, and other documents can be placed, in order to lessen the load on the WSGI Django server. Your server is likely different than mine, so you will have to tailor these steps as necessary.

The Nginx configuration file

This is an Nginx configuration file for a single Django project. You can add in whatever locations-alias blocks you like.

server {
   server_name 104.131.200.120;

   access_log on;

   location /auth_lifecycle/static/ {
       alias /home/myname/django_auth_lifecycle/root/static/;
   }

   #Static images for the admin
   location /static/admin/ {
       alias /home/myname/django_auth_lifecycle/venv/lib/python3.4/site-packages/django/contrib/admin/static/admin/;
   }

   location / {
       proxy_pass http://127.0.0.1:8001;
       proxy_set_header X-Forwarded-Host $server_name;
       proxy_set_header X-Real-IP $remote_addr;
       add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
   }
}

The steps I take:

  1. sudo nano /etc/nginx/sites-available/django_project_name
  2. Replace the contents in that file with the above text.
  3. Save and close it with Ctrl+X and then Enter.
  4. Create the directory
        /home/myname/django_auth_lifecycle/root/static/
  5. Restart Nginx: sudo service nginx restart
  6. To confirm your setup, create a file named
        /home/myname/django_auth_lifecycle/root/static/temp.txt
    and put something in it, such as with

        cat x > /home/myname/django_auth_lifecycle/root/static/temp.txt
  7. In your browser, you should be able to see that text at the url
        http-colon-slash-slashmy.website/auth_lifecycle/static/temp.txt
    Delete the file before proceeding.

Since the (non-admin) static location is set to /auth_lifecycle/static/, this must be mirrored in Django, by setting this same value to the STATIC_URL variable in settings.py:

STATIC_URL = "/auth_lifecycle/static/"

An aside: Since this configuration really belongs to this Django project, I recommend storing the above configuration text in its entirety, in settings.py, (in a multi-line comment: """..."""), immediately above the STATIC_URL variable.

Referencing static files in a Django template

Although you could use

<link href="{{ STATIC_URL }}stylesheets/tabs.css" ...>

You should instead use the “static” template tag:

{% load staticfiles %}
{% static 'stylesheets/tabs.css' %}

Django AJAX/JQuery tutorial: JavaScript rapid-fire protection updated to Underscore.js (original code in this post)

The JavaScript in the Django AJAX/JQuery tutorial protects against rapid-fire button clicks (direct link to the JS source for the like-buttons in part two in search-box in part three). It originally did this protection in a manual fashion, and has now been updated to use Underscore.jsdebounce function. The benefit of debounce is that it eliminates all the “protection” sections in the manual version, making the code cleaner and easier to understand.

What I thought was a minor protection against a real DDOS attack, is in fact nothing of the sort. These kinds of attack directly target the server, they don’t go through the client (browser) at all. Therefore client-side protection is pointless. It is, however, a reasonable protection against users doing things as fast as they can.

Here’s the original, manual code for reference:

color_ajax_like.js

//THIS FILE MUST BE IMPORTED BEFORE THE "main" FILE.

/**
    The number of milliseconds to ignore clicks on the *same* like
    button, after a button *that was not ignored* was clicked. Used by
    <link to processLike>

    Equal to <code>500</code>.

    The disabling and re-enabling is logged to the console.
 */
var MILLS_TO_IGNORE_LIKES = 500;
/**
    Executes a like click. Triggered by clicks on the various yes/no
    links.

    The disabling and re-enabling is logged to the console.

    See <link to MILLS_TO_IGNORE_LIKES>
 */
var processLike = function()  {

    //In this scope, "this" is the button just clicked on.
    //The "this" in processLikeInner is *not* the button just clicked on.
    var $button_just_clicked_on = $(this);

    //The value of the "data-color_id" attribute.
    var color_id = $button_just_clicked_on.data('color_id');

    var processLikeInner = function(data, textStatus_ignored, jqXHR_ignored)  {
        //alert("sf data='" + data + "', textStatus_ignored='" + textStatus_ignored + "', jqXHR_ignored='" + jqXHR_ignored + "', color_id='" + color_id + "'");
        $('#toggle_color_like_cell_' + color_id).html(data);

        //Rapid-fire click prevention. Don't process requests too close together.

        console.log('Like disabled for: ' + MILLS_TO_IGNORE_LIKES);

        setTimeout(function() {
            $button_just_clicked_on.one('click', processLike);
            console.log('Like re-enabled for color_id ' + color_id + ' ');
        }, MILLS_TO_IGNORE_LIKES);
    }

    var config = {
        url: LIKE_URL_PRE_ID + color_id + '/',
        dataType: 'html',
        success: processLikeInner
    };
    $.ajax(config);
};

color_ajax_search.js

//THIS FILE MUST BE IMPORTED BEFORE THE "main" FILE.

/**
    The number of milliseconds to ignore key-presses in the search box,
    after a key *that was not ignored* was pressed. Used by
    <link to processSearch>

    Equal to <code>100</code>.

    The disabling and re-enabling is logged to the console.
 */
var MILLS_TO_IGNORE_SEARCH = 100;
/**
    Executes a search for colors containing a substring. Triggered by
    key-presses in the search box, as long as there are MIN_SEARCH_CHARS
    or more characters in it (which are lowercased, and trimmed before
    submission).

    See <link to MILLS_TO_IGNORE_SEARCH>.
 */
var processSearch = function()  {
    //The key-press listener is no longer attached

    //Get and trim the search text.
    var searchText = $('#color_search_text').val().trim().toLowerCase();

    if(searchText.length < MIN_SEARCH_CHARS)  {
        //Too short. Ignore the submission, and erase any current results.
        $('#color_search_results').html("");

        //No need to prevent the next request. The server/database haven't
        //been hit.
        activateSearchListenerOnce();

    }  else  {
        //There are at least two characters. Execute the search.
        var config = {
            /*
                Using GET allows you to directly call the search page in
                the browser:

                http://the.url/search/?color_search_text=bl

                Also, GET-s do not require the csrf_token
             */
            type: "GET",
            url: SUBMIT_URL,
            data: {
                'color_search_text' : searchText,
            },
            dataType: 'html',
            success: function (data, textStatus_ignored, jqXHR_ignored)  {
                //alert("data='" + data + "', textStatus_ignored='" + textStatus_ignored + "', jqXHR_ignored='" + jqXHR_ignored + "'");
                $('#color_search_results').html(data);
            }
        };
        $.ajax(config);

        //Attack prevention. Don't process requests too close together.

        console.log('Search disabled for: ' + MILLS_TO_IGNORE_SEARCH);
        setTimeout(function() {
            activateSearchListenerOnce();
            console.log('Search re-enabled');
        }, MILLS_TO_IGNORE_SEARCH);
    }
};

color_ajax_main.js

//THIS MUST BE IMPORTED AS THE VERY LAST THING BEFORE THE CLOSE </body>
//tag.

/**
    Attach the search-textbox listener. Used by the main function and
   color_ajax_search.js/processSearch.
 */
var activateSearchListenerOnce = function()  {
     $('#color_search_text').one('keyup', processSearch);
}
/**
   The Ajax "main" function. Attaches the listeners to the elements on
   page load.
 */
$(document).ready(function()  {
    activateSearchListenerOnce();
    /*
        There are many buttons having the class

            td__toggle_color_like_button

        This attaches a listener to *every one*. Calling this again
        would attach a *second* listener to every button, meaning each
        click would be processed twice.

        When a button is clicked, the listener for that *single button*
        is disabled. It's re-enabled in processLikeInner with

            $button_just_clicked_on.one('click', processLike);
     */
    $('.td__toggle_color_like_button').one('click', processLike);
});

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)

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)

From fixtures to factories: Updating testing code from manual data fixtures to Fixture Boy

In part one of my Django authentication/testing tutorial, the testing code originally contained a manually-created fixture of test users, including a function to directly save one to the database.

TEST_USER_GIBBERISH = {
    "username": "3u9osuhceo",  "password":  "8um48eut93te",
    "first_name": "xrc4b46id", "last_name": "agc4598pdb9uyfn",
    "birth_year": 1931,        "email":     "oaudetnnao@oth09867.com"}
TEST_USER_KERMIT = {
    "username":   "kermit",    "password":  "timrek",
    "first_name": "Kermit",    "last_name": "The Frog",
    "birth_year": 1955,        "email":     "kermit@muppets.com"}
TEST_USER_FOZZIE = {
    "username":   "fozzie",    "password":  "eizzof",
    "first_name": "Fozzie",    "last_name": "Bear",
    "birth_year": 1976,        "email":     "fozzie@muppets.com"}
TEST_USERS = [TEST_USER_GIBBERISH, TEST_USER_KERMIT, TEST_USER_FOZZIE]

For the purpose of the authentication tutorial, manual fixtures are just fine. In the real world, they should be avoided. As described in Carl Meyer’s excellent talk on Django testing, from PYCON 2012, at 16:20, manually-created fixtures are difficult to maintain, slow to load, and dangerous to share among tests.

Instead, model factories should be used. I’m using Factory Boy. The main benefits of model factories are:

  • When the model changes, the factory automatically detects this, always returning a properly formatted object.
  • Although you must specify defaults for each field, in the definition of each factory, individual tests do not need to provide specific values, expect for only those attributes requiring special values in order to pass. See the top-most example on the main Factory Boy documentation page.
  • Objects can be easily created in memory only, or saved to the database, as required by your tests.

The question is moot.

In addition to fixtures, I also changed it so the user objects are no longer held locally; they are now only created in the database. I thought it was important to have local copies of these objects, so the database values could be compared against them. It’s not (and doing so created problems because the local object was static)-trust Django and Factory Boy to insert them properly. This therefore renders the model tests moot, so the entire ModelsTestCase class has been eliminated, and the file has therefore been renamed to `test__utilities.py`

Below is the original, manual-fixture file in its entirety, test__utilities.py. The updated, Fixture Boy version, is now in part one of the tutorial.

"""
Tests for models.py.

DEPENDS ON TEST:  *nothing* (must not depend on any test_*.py file)
DEPENDED ON TEST: test__profile.py

To run these tests:
  1. source /home/myname/django_files/django_auth_lifecycle_venv/bin/activate
  2. cd /home/myname/django_files/django_auth_lifecycle/
  3. python -Wall manage.py test auth_lifecycle.test__utilities

Running tests documentation:
- https://docs.djangoproject.com/en/1.7/topics/testing/overview/#running-tests

Information on '-Wall' is at the bottom of this section in the official
Django documentation:
- https://docs.djangoproject.com/en/1.7/topics/testing/overview/#running-tests

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 django.test                import TestCase
from auth_lifecycle.models      import UserProfile
from django.contrib.auth.models import User
from .models                    import MIN_BIRTH_YEAR

TEST_USER_GIBBERISH = {
    "username": "3u9osuhceo",  "password":  "8um48eut93te",
    "first_name": "xrc4b46id", "last_name": "agc4598pdb9uyfn",
    "birth_year": 1931,        "email":     "oaudetnnao@oth09867.com"}
"""
The main test user used by all tests, containing only gibberish values.

The reason for the gibberish values is to easily and confidently
extract them from the webpage, without having to somehow
delineate them, such as with marker comments like

<!-- UNITRQD-start: email -->email@something.com<!-- UNITRQD-end -->

See this comment, and the entire question-post it's part of:
- http://codereview.stackexchange.com/q/66898/35364#comment121964_66917
"""
TEST_USER_KERMIT = {
    "username":   "kermit",    "password":  "timrek",
    "first_name": "Kermit",    "last_name": "The Frog",
    "birth_year": 1955,        "email":     "kermit@muppets.com"}
TEST_USER_FOZZIE = {
    "username":   "fozzie",    "password":  "eizzof",
    "first_name": "Fozzie",    "last_name": "Bear",
    "birth_year": 1976,        "email":     "fozzie@muppets.com"}
TEST_USERS = [TEST_USER_GIBBERISH, TEST_USER_KERMIT, TEST_USER_FOZZIE]
"""
An array of users for testing purposes only. Each element is a dictionary
containing all non-id attributes in both the User and UserProfile models.

Equal to `[TEST_USER_GIBBERISH, TEST_USER_KERMIT, TEST_USER_FOZZIE]`

Creating the test users in this way allows us to centralize their
attributes, so we don't have hard-coded passwords in multiple places
throughout the testing code, for example.

User model attributes:

https://docs.djangoproject.com/en/1.7/topics/auth/default/#user-objects
"""
def _insert_test_user(test_user):
    """
    Insert and save a single test user.

    Private function for this file only.
    """
    user = User.objects.create_user(
        username=test_user['username'], password=test_user['password'],
        first_name=test_user['first_name'],
        last_name=test_user['last_name'], email=test_user['email'])

    """
    Creating the user with
        user = User(...)
        user.save()
    does not properly hash and salt the password. Although it does save it
    to the database, attempting
        self.client.login(username='theusername', password='thepassword')
    fails (returns False)

    https://docs.djangoproject.com/en/1.7/ref/contrib/auth/#django.contrib.auth.models.UserManager.create_user
    http://stackoverflow.com/questions/26306424/cant-login-a-just-created-user-in-a-django-test
    """

    #The user's id is automatically created by the database. To function
    #as the foreign key, it must be duplicated to the profile.

    profile = UserProfile(user_id=user.id, birth_year=test_user['birth_year'])
    profile.save()

def create_insert_test_users():
    """Insert all <link to TEST_USERS> into the test-database."""
    for  test_user in TEST_USERS:
        #print(test_user)
        _insert_test_user(test_user)

class ModelsTestCase(TestCase):
    """Tests for models.py."""
    def setUp(self_ignored):
        """Insert test users."""
        create_insert_test_users()

    def test_all_demo_users_inserted(self):
        """
        The birth year in the database should equal the local TEST_USERS
        dictionary value.
        """
        for  test_user in TEST_USERS:
            user = User.objects.get(username=test_user['username'])
            self.assertEqual(test_user['birth_year'], user.profile.birth_year)

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

In parts one and two of this tutorial, we created the model and the first of our two views: The user’s profile page, which is only viewable to logged-in users. In this post, we’ll create the other half of our trivial website: the “main” page, which contains aggregate information about all website users and, if they happen to be logged in, some extra private info.

This is the final post where we don’t deal with authentication.

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

screenshot

Create the view

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

"""
Renders the main page for the user-authentication-lifecycle project,
containing aggregate information about all the website's users, with
additional private information for logged-in users.
"""
from .view__profile        import TMPL_BASE_DIR
from auth_lifecycle.models import UserProfile
from django.db.models      import Avg
from django.shortcuts      import render
from django.template       import RequestContext
def get_avg_birth_year():
    """Returns the average year of birth for all website users."""
    return  UserProfile.objects.all().aggregate(Avg('birth_year'))['birth_year__avg']

def get_rendered(request):
    """
    Displays aggregate information about the birth year of all users on
    this website. The average birth year is publicly displayed. The
    difference between a single user's birth year and the average (and
    a link to their profile page), is only displayed to logged-in
    users.

    General information on aggregation queries:
    - https://docs.djangoproject.com/en/1.7/topics/db/aggregation/"

    It's not used here, but an interesting related post on doing
    GROUP BY in Django:
    - http://blog.roseman.org.uk/2010/05/10/django-aggregation-and-simple-group/"
    """
    context = RequestContext(request)

    #Publicly-viewable information
    avg_birth_year = get_avg_birth_year()
    context['user_count'] = UserProfile.objects.count()
    context['avg_birth_year'] = avg_birth_year

    if  request.user.is_authenticated():
        """
        The user is logged in. Display extra private information. See

        https://docs.djangoproject.com/en/1.7/ref/contrib/auth/#django.contrib.auth.models.AnonymousUser"

        Retrieve the logged-in-only information.

        Interesting discussion about the 80-chars-per-line maximum of
        PEP-8, which the following line violates:
        - http://chat.stackoverflow.com/transcript/message/19414851#19414851
        """
        user_birth_year = UserProfile.objects.get(user_id=request.user.id).birth_year
        context['birth_year_diff_from_avg'] = user_birth_year - avg_birth_year

    """
    else: They're not logged in. While the above information *could* be
          safely passed to the template even if they're not logged in
          (because the template also properly suppresses it), why retrieve
          when we know it's not going to be used?
    """

    return  render(request, TMPL_BASE_DIR + 'main_page.html',
                   context_instance=context)

Create the template

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

{% load i18n %}       {# For the "trans" tag #}
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>{% trans "Main page" %}</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="viewport" content="width=device-width"/>
    </head>
<body>
    <h2>{% trans "Main page" %}</h2>

    <p>{% trans "This website has" %} {{ user_count }}
    {% trans "users total, with an average birth year of" %} {{ avg_birth_year }}.</p>

    {% if  user.is_authenticated %}
        <h2>{% trans "Private information for logged-in user named" %}
        &quot;{{ user.username }}&quot; <i>{% trans "only" %}</i></h2>

        <p>{% trans "Welcome" %}, {{ user.username }}!
        {% trans "Your birth year is" %} {{ user.profile.birth_year }},
        {% trans "which is" %} {{ birth_year_diff_from_avg }}
        {% trans "years different from average" %}.</p>

        <p>{% trans "View" %}
        <a href="{% url 'user_profile' %}">{% trans "your profile" %}</a>
        {% trans "or ...logout" %}...</p>

    {% else %}
        <p><i><b>...{% trans "Login" %}...</b>
        {% trans "to view your profile, and to compare your birth year to the average" %}.</i></p>
    {% endif %}

</body>
</html>

Create the tests

As in the profile tests, there is a logged-in test and a not-logged-in test. In the main page, however, there is always something displaying, so the sub-tests that check for the public information are placed in a separate function.

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

"""
Tests for the main page.

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.test__view_main

See the top of <link to .test__utilities> for more information.
"""
from .models                  import UserProfile
from .test__utilities         import assert_attr_val_in_content
from .test__utilities         import create_insert_test_users, UserFactory
from .test__utilities         import debug_test_user, login_get_next_user
from .view__main              import get_avg_birth_year
from django.core.urlresolvers import reverse
from django.test              import TestCase

def _get_page_response(test_instance):
    """
    Returns the response object for the main page.

    Private function for this file only.
    """
    return  test_instance.client.get(reverse('main_page'))

def _subtest_public_info_exists(test_instance, content_str):
    """
    Publicly-accessible information should be somewhere on the page.

    Private function for this file only.
    """
    #The number of users and average birth year exist
    test_instance.assertTrue(str(UserProfile.objects.count()) in content_str)
    test_instance.assertTrue(str(get_avg_birth_year()) in content_str)

class MainPageTestCase(TestCase):
    """Tests for the main page."""
    def setUp(self_ignored):
        """Insert test users."""
        create_insert_test_users()

    def test_page_responds_with_200(self):
        """
        The main birth-stats page should give a 200 response at all
        times.
        """
        response = _get_page_response(self)
        self.assertEqual(200, response.status_code)

    def test_content_not_logged_in(self):
        """Public information should be somewhere on the page."""

        self.client.logout()

        content_str = str(_get_page_response(self).content)

        #print("content_str: " + content_str)

        _subtest_public_info_exists(self, content_str)

        #Logged-in information should not be displaying
        self.assertTrue(UserFactory().username not in content_str)

    def test_logged_in_users(self):
        """
        In addition to public information, private content for logged in
        users should also be somewhere on the page.
        """

        #Test the first two users
        for  n in range(2):
            test_user = login_get_next_user(self)
            content_str = str(_get_page_response(self).content)

            #print("content_str: " + str(content_str))

            _subtest_public_info_exists(self, content_str)

            #The test user's attributes should be somewhere on the page.
            birth_year = test_user.profile.birth_year
            assert_attr_val_in_content(self, 'username', test_user.username, content_str)
            assert_attr_val_in_content(self, 'birth_year', birth_year, content_str)

            #The difference between the user's birth year and the average
            #should be somewhere on the page.
            difference = birth_year - get_avg_birth_year()
            self.assertTrue(str(difference) in content_str)

Configure the urls

Add the main page element to the patterns list in
    /home/myname/django_auth_lifecycle/djauth_root/auth_lifecycle/urls.py

url(r'^$', 'auth_lifecycle.view__main.get_rendered', name='main_page'),

The only difference is the addition of the 'main_page' line.

Run all the tests we’ve created so far

  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

Output:

/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)
Creating test database for alias 'default'...
.....
----------------------------------------------------------------------
Ran 5 tests in 2.127s

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

(The deprecation warning refers to something outside of our code, so we’re going to ignore it. Our tests passed.)

Insert some data into the database

(If data already exists, delete it via the admin app (http://my.website/admin)–just don’t delete the ‘admin’ superuser!)

We’ve already created the code to insert users and their profiles into the database. It’s the create_insert_test_users function in test__utilities.py, as used by every test. So let’s use it.

$ source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate
$ cd /home/myname/django_auth_lifecycle/djauth_root/
$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> from auth_lifecycle.models import UserProfile
>>> User.objects.all()
[<User: admin>]
>>> UserProfile.objects.all()
[<UserProfile: admin>]
>>> from auth_lifecycle.test__utilities import create_insert_test_users
>>> create_insert_test_users()
>>> User.objects.all()
[<User: admin>, <User: 3u9osuhceo>, <User: kermit>, <User: fozzie>]
>>> UserProfile.objects.all()
[<UserProfile: admin>, <UserProfile: 3u9osuhceo>, <UserProfile: kermit>, <UserProfile: fozzie>
>>> exit()

Give it a try!

We can finally take a look at our website in a web browser:

  1. Start the webserver:
    • For Nginx/Gunicorn (may be executed in or out of the virtualenv):
      1. sudo service nginx start
      2. sudo /home/myname/django_auth_lifecycle/djauth_venv/bin/gunicorn -c /home/myname/django_auth_lifecycle/djauth_venv/gunicorn_config.py django_auth_lifecycle.wsgi:application     (the contents of the config file)
    • For the development-only Django server:
      1. Make sure you’re in the virtualenv:
            source /home/myname/django_auth_lifecycle/djauth_venv/bin/activate
      2. cd /home/myname/django_auth_lifecycle/djauth_root/
      3. python manage.py runserver
    • Open the browser and load the page:
          http-colon-slash-slashmy.website/auth_lifecycle/

This displays the main page (screenshot at the top of this post), with public information only. Attempting to go to
    http-colon-slash-slashmy.website/auth_lifecycle/user_profile
redirects you right back to the main page. If you really want to see the profile page before the login view exists, take a look at the workaround in the comments of view__profile.get_rendered.

In the next post, we’ll create the beginning of a login page.

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

…to be continued…

(cue cliffhanger segue music)