I am writing a simple Django project which has no authentication at all. It's going to be used as the basis of a tutorial I'm writing, on the full authentication lifecycle (create account, login, change/reset password, delete account, etc.), where testing will be emphasized at every step. Beyond testing, the goal of this tutorial is to create as little of a website as possible, before moving onto the authentication parts.
This first question-post contains only the model, its tests, the urls, and settings. After these are improved, I'll follow up with a single view and its tests.
These are the only two views:
The "main" page which is publicly viewable, but shows extra information when logged in. This screenshot demonstrates being logged in (the login functionality doesn't exist yet, but a few days ago I manually logged a user in with authenticate
and then login
, and the cookie is still in my browser).
It shows some aggregate information, compares their information to the aggregate, and provides a link to their profile page.
Displays all their available information, aside from password, and links back to the main page.
I've built a few demo Django websites, nothing production. However, testing is completely new to me (both in Python and Django). While comments on anything and everything would be appreciated, I'm mostly interested in improving the tests and ensuring the Django code is Django-y. In particular, I'm wondering if there might be a more standard/concise way to do the "unit-test-required blocks" (<!--UNITRQD: start...
) as you'll see in the templates (in a follow up post).
I come from backend (non-GUI) Java. I've learned both Python and Django in the past few months, hand in hand, at the same time.
pylint
has been run on everything, and all tests pass.
Model
Beyond the User
object, the only extra piece of information in the model is their birth year, which is in a UserProfile
model.
models.py
"""Defines a single extra user-profile field for the user-authentication
lifecycle demo project:
- 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
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(value):
"""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.
See:
https://docs.djangoproject.com/en/1.7/ref/validators/
"""
intval = -1
try:
intval = int(str(value).strip())
except TypeError:
raise ValidationError(u'"{0}" is not an integer'.format(value))
if intval < MIN_BIRTH_YEAR or intval > MAX_BIRTH_YEAR:
message = (u'{0} is an invalid birth year.'
u'Must be between {1} and {2}, inclusive')
raise ValidationError(message.format(
value, MIN_BIRTH_YEAR, MAX_BIRTH_YEAR))
#It's all good.
model.py
continued. The model class:
class UserProfile(models.Model):
"""Extra information about a user: Birth year and profile picture. See
the package doc for more info.
---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
test__models.py
"""Tests for models.py.
DEPENDS ON: *nothing* (must not depend on any test_*.py file)
DEPENDED ON BY: test_view_birth_stats.py
pylint auth_lifecycle.test__models > 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
TEST_USERS = [
{"username": "kermit", "password": "timrek",
"first_name": "Kermit", "last_name": "The Frog",
"birth_year": 1955, "email": "[email protected]"},
{"username": "fozzie", "password": "eizzof",
"first_name": "Fozzie", "last_name": "Bear",
"birth_year": 1976, "email": "[email protected]"}
]
"""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.
Creating the test users in this way allows us to centralize all 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.
"""
"""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)
See
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
"""
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'])
#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 insert_all_test_users():
"""Utility function for inserting all test users in <link to TEST_USERS>."""
for test_user in TEST_USERS:
#print(test_user)
_insert_test_user(test_user)
test_models.py
, continued. The test class:
class ModelsTestCase(TestCase):
"""Tests for models.py."""
def setUp(self_ignored):
"""Insert test users."""
insert_all_test_users()
def test_all_demo_users_inserted(self):
"""Verify the database birth year equals the local test-user
birth year.
"""
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 project's urls.py
.../django_auth_lifecycle/django_auth_lifecycle/urls.py
from django.conf.urls import patterns, include, url
from django.contrib import admin
urlpatterns = patterns('',
#Adding the namespace attribute to this element would for all
#references to be prefixed with "auth_lifecycle:". In the template:
# {% url 'auth_lifecycle:url_name' %}
#Elsewhere:
# reverse('auth_lifecycle:url_name'))
#See:
#https://docs.djangoproject.com/en/1.7/topics/http/urls/#url-namespaces
#
#(Unrelated note: If this were a multi-line comment, it would cause
# a syntax error.)
url(r'^auth_lifecycle/', include('auth_lifecycle.urls')),# namespace="auth_lifecycle")),
url(r'^admin/', include(admin.site.urls)),
)
The sub-app's urls.py
.../django_auth_lifecycle/auth_lifecycle/urls.py
from django.conf.urls import patterns, url
urlpatterns = patterns('',
url(r"^$", "auth_lifecycle.views.birth_year_stats", name="birth_year_stats"),
url(r"^user_profile/$", "auth_lifecycle.views.user_profile", name="user_profile"),
)
settings.py
"""
Django settings for django_auth_lifecycle project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '+dqj*v^5^3dqow9%zuc06p3mym4t353u1098%)zv!&^o(y$r1_'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'auth_lifecycle',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
TEMPLATE_DIRS = (
BASE_DIR + '/auth_lifecycle/templates/',
)
ROOT_URLCONF = 'django_auth_lifecycle.urls'
WSGI_APPLICATION = 'django_auth_lifecycle.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
# Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
# Or path to database file if using sqlite3.
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'auth_lifecycle_db',
# The following settings are not used with sqlite3:
'USER': 'auth_lifecycle_db_user',
'PASSWORD': '5*HlH3`U}$km7aIz3{HADFji4c',
'HOST': "localhost", # Empty for localhost through domain sockets or
#'127.0.0.1' for localhost through TCP.
'PORT': '', # Set to empty string for default.
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = ""
MEDIA_ROOT="/home/jeffy/django_files/django_auth_lifecycle/uploaded_files/"
"""Put the following in
`/etc/nginx/sites-available/django_auth_lifecycle`
----------------------------------------
server {
server_name my.websites.ip.or.domain;
access_log on;
#Django User-Authentication Lifecycle Demo...START
#Static images for the admin
location /static/admin/ {
alias /home/jeffy/django_files/django_auth_lifecycle_venv/lib/python3.4/site-packages/django/contrib/admin/static/admin/;
}
#Django User-Authentication Lifecycle Demo...END
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"';
}
}
----------------------------------------
This prevents nginx passing the request to the WSGI/Django app
server. Static files need no processing, so nginx handles them
directly.
"""