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)

Advertisements

Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s