Learn » Django » Django for UW.md

Django for UW

We will walk through building your first Django project together. This is built on top of the Django tutorial so you review that for another treatment of the same material.

We are assuming you have access to pip and virtualenv on your system.

Setup Virtualenv

To make sure things work correctly we will use bash as our standard shell.

bash

If you open a new terminal window make sure you run this first.

Start in your home directory, ~/ you can get there by typing cd. From there lets create a folder to work in.

mkdir development
cd development

First lets create a directory for virtualenvs.

mkdir venvs
cd venvs
virtualenv first_app
source first_app/bin/activate

Now you should see (first_app) infront of your command prompt. For now on when we run pip or python we will be writing from inside this virtualenv.

Install Django and setup the project

Go back to your home directory, ~/ you can get there by typing cd. Now let's install the most recent version of Django (1.6.4).

pip install django

# Various output

Successfully installed django
Cleaning up...

Now let's create a folder for the Django code beside the virtualenv.

cd development
mkdir django
cd django

In ~/development/django we can create our first project.

django-admin.py startproject demo

This has created a few files and the following structure on disk

demo    (<--  the project)
    demo    (<-- the application)
        __init__.py
        settings.py
        urls.py
        wsgi.py
    manage.py

Let's add our own requirements.txt file beside manage.py to keep track of the version of Django we installed for this project.

cd demo
vi requirements.txt

Inside requirements.txt type this one line.

django==1.6.4

requirements.txt files are a great complement to virtualenvs because you can run pip install -r requirements.txt to install all of the requirements for a specific project.

Review settings.py

Let's start by looking at settings.py. This is the cleaning house for fundamental information about your Django project like which apps it is using.

"""
Django settings for demo project.

For more information on this file, see
https://docs.djangoproject.com/en/1.6/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.6/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.6/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'mk01oe-pok_8*^ll4e!ka9zdkh7o!9^^9)usq705)tuglaar1('

# 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',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

ROOT_URLCONF = 'demo.urls'

WSGI_APPLICATION = 'demo.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.6/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

# Internationalization
# https://docs.djangoproject.com/en/1.6/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.6/howto/static-files/

STATIC_URL = '/static/'

This file is just Python, but basically it defines a bunch of variables which are accessible through out your Django application.

Intead of making an additional app. we are going to put the views and models for our first app inside demo, so first we need add demo as an INSTALLED_APP.

INSTALLED_APPS = (
    'demo', # Add this one line.
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
)

Soon we will add models.py, views.py and admin.py to the demo directory, but let's get Django running first.

Run your first Django project

You want to be in the folder with manage.py in it such that you can run

python manage.py

and it will output a list of available commands.

The first step before running a Django project for the first time is to create the database. We will be using Sqlite3 which comes built in to Python, so no setup is required.

python manage.py syncdb
Creating tables ...
Creating table django_admin_log
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
Creating table auth_user_groups
Creating table auth_user_user_permissions
Creating table auth_user
Creating table django_content_type
Creating table django_session

You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'username'): username
Email address: youremail@uwaterloo.ca
Password: 
Password (again): 
Superuser created successfully.
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

This created some empty tables and one user, which will be handy to log in to the admin with.

python manage.py runserver 8100 # Use your specific port
Validating models...

0 errors found
April 27, 2014 - 00:17:47
Django version 1.6.3, using settings 'demo.settings'
Starting development server at http://127.0.0.1:8100/
Quit the server with CONTROL-C.

Now you should be able to open up http://127.0.0.1:8100/ (your port may be different) and see your first Django powered webpage.

Pretty exciting, but even more exciting is the admin which is already running at http://127.0.0.1:8100/admin/. You can log in with the creds you just provided.

Models

The next step on making our own Django app is defining the models which are Python classes which describe the data we want to store in the database.

cd into demo/demo (the application) and write the following in models.py

from django.db import models

class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __unicode__(self):
        return self.question

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __unicode__(self):
        return self.choice_text

The code is straightforward. Each model is represented by a class that subclasses django.db.models.Model. Each model has a number of class variables, each of which represents a database field in the model.

Each field is represented by an instance of a Field class – e.g., CharField for character fields and DateTimeField for datetimes. This tells Django what type of data each field holds.

The name of each Field instance (e.g. question or pub_date) is the field’s name, in machine-friendly format. You’ll use this value in your Python code, and your database will use it as the column name.

You can use an optional first positional argument to a Field to designate a human-readable name. That’s used in a couple of introspective parts of Django, and it doubles as documentation. If this field isn’t provided, Django will use the machine-readable name. In this example, we’ve only defined a human-readable name for Poll.pub_date. For all other fields in this model, the field’s machine-readable name will suffice as its human-readable name.

Some Field classes have required arguments. CharField, for example, requires that you give it a max_length. That’s used not only in the database schema, but in validation, as we’ll soon see.

A Field can also have various optional arguments; in this case, we’ve set the default value of votes to 0.

Finally, note a relationship is defined, using ForeignKey. That tells Django each Choice is related to a single Poll. Django supports all the common database relationships: many-to-ones, many-to-manys and one-to-ones.

Let's see what it takes to add these new models to our database. cd back into demo (the project) and run the manage.py command.

python manage.py sql demo
BEGIN;
CREATE TABLE "demo_poll" (
    "id" integer NOT NULL PRIMARY KEY,
    "question" varchar(200) NOT NULL,
    "pub_date" datetime NOT NULL
)
;
CREATE TABLE "demo_choice" (
    "id" integer NOT NULL PRIMARY KEY,
    "poll_id" integer NOT NULL REFERENCES "demo_poll" ("id"),
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL
)
;

COMMIT;

We can run this sql with syncdb

python manage.py syncdb

Using the builtin admin

Using the admin with your own models is pretty easy, all you need a an admin.py file beside your apps models.py file. Here is what is should contain.

from django.contrib import admin

from .models import Poll, Choice

admin.site.register([Poll, Choice])

That is all you need to register models with the Django admin.

You can run the server again and view the updated admin.

python manage.py runserver 8100 # Use your specific port

You can also define an Admin class which overrides the default behaviour including inlines which are handy for the way these models are arranged.

Change admin.py to read like this.

from django.contrib import admin

from .models import Choice, Poll

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3

class PollAdmin(admin.ModelAdmin):
    inlines = [ChoiceInline]

admin.site.register(Poll, PollAdmin)

Now choices will appear on the same page as the associated poll object in the admin.

Sign using CAS

At UW being able make Django apps which use CAS is pretty handy, but if you are running behind it probably makes sense to fastforward to the next section and come back here when you have time.

Add -e hg+https://bitbucket.org/amjoconn/django-cas#egg=django_cas to requirements.txt and then run pip install -r requirements.txt.

django==1.6.4
-e hg+https://bitbucket.org/amjoconn/django-cas#egg=django_cas

Next update your settings.py.

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django_cas.middleware.CASMiddleware', # Add the CASMiddleware to enable admin login by CAS.
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

Add some new setting to settings.py.

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'django_cas.backends.CASBackend',
)

CAS_SERVER_URL = "https://cas.uwaterloo.ca/cas/"

Update urls.py with the following.

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'demo.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    # Add these two lines
    url(r'^accounts/login/$', 'django_cas.views.login'),
    url(r'^accounts/logout/$', 'django_cas.views.logout'),

    url(r'^admin/', include(admin.site.urls)),
)

Now when you start the server and logout and then back into the admin it will use CAS.

python manage.py runserver 8100 # Use your specific port

Your superuser username will need to be the same as your CAS username to work. If you need to create a new superuser you can with a management command.

python manage.py createsuperuser

Simple form processing

Let's write our own view, it will process a form letting people vote.

This will require a new URL pattern in your urls.py file, a new views.py file and a new template file. Let's start with urls.py.

from django.conf.urls import patterns, include, url

# Add this import
from .views import vote

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'demo.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    # Add url line for voting
    url(r'^(?P<poll_id>\d+)/vote/$', vote, name='vote'),

    url(r'^accounts/login/$', 'django_cas.views.login'),
    url(r'^accounts/logout/$', 'django_cas.views.logout'),

    url(r'^admin/', include(admin.site.urls)),
)

This won't work right now because .views isn't importable, but we can change that by adding views.py beside models.py.

from django.shortcuts import get_object_or_404, render, redirect
from .models import Choice, Poll


def vote(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the poll voting form.
        return render(
            request, 
            'polls/vote.html', 
            dict(
                poll=p,
                error_message="You didn't select a choice.",
            )
        )
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return redirect('vote', p.id)

All we need is the template, polls/vote.html. First need to create folder for templates under demo and then a folder for polls.

mkdir -p demo/templates/polls
vi demo/templates/polls/vote.html

Inside the template place this Django template code.

<h1>{{ poll.question }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'vote' poll.id %}" method="post">
{% csrf_token %}
{% for choice in poll.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

With all these pieces in play we should have a view where you can see a poll and vote on it. Run the server and go to /1/vote/ assuming you have created a poll via the admin.

python manage.py runserver 8100 # Use your specific port

Right now our user feedback isn't good, when you vote you get taken back to the voting form. For our last step we will build a basic view that will graph the current results.

Graph the results

We are going to need another view to just display the current results and then another template which includes some fancy Javascript to make a chart.

Django doesn't care what you use for the frontend, though the admin does use jQuery.

First we will need another URL to route to our new view.

from django.conf.urls import patterns, include, url

# Add this import
from .views import vote, results

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'demo.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    # Add url line for results
    url(r'^(?P<poll_id>\d+)/$', results, name='results'),
    url(r'^(?P<poll_id>\d+)/vote/$', vote, name='vote'),

    url(r'^accounts/login/$', 'django_cas.views.login'),
    url(r'^accounts/logout/$', 'django_cas.views.logout'),

    url(r'^admin/', include(admin.site.urls)),
)

In views.py add view called results.

from django.shortcuts import get_object_or_404, render, redirect
from .models import Choice, Poll


def vote(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    #... Keep your code here from before


def results(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)

    return render(
        request,
        'polls/results.html',
        dict(
            poll=p,
        )
    )

Finally we need a template called polls/results.html. Lets start with a basic HTML version.

vi demo/templates/polls/results.html

Inside let's start with this basic html page.

<h1>{{ poll.question }}</h1>

<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice_text }} {{ choice.votes }}</li>
{% endfor %}
</ul>
<a href="{% url 'vote' poll.id %}">Vote</a>

As a final step lets up date our vote view to redirect to our results view.

from django.shortcuts import get_object_or_404, render, redirect
from .models import Choice, Poll


def vote(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the poll voting form.
        return render(
            request,
            'polls/vote.html',
            dict(
                poll=p,
                error_message="You didn't select a choice.",
            )
        )
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return redirect('results', p.id) # Change this line to redirect to results

Now when you vote you get taken back to the results page which provides for a better user experience.

Finally, let's use D3 to graph the results. We will need to edit polls/results.html to be like this.

<style>

.chart div {
  font: 10px sans-serif;
  background-color: steelblue;
  text-align: right;
  padding: 3px;
  margin: 1px;
  color: white;
}

</style>
<div class="chart"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>

var data = [{% for choice in poll.choice_set.all %}{{ choice.votes}}{% if not forloop.last %},{% endif %}{% endfor %}];

var x = d3.scale.linear()
    .domain([0, d3.max(data)])
    .range([0, 420]);

d3.select(".chart")
  .selectAll("div")
    .data(data)
  .enter().append("div")
    .style("width", function(d) { return x(d) + "px"; })
    .text(function(d) { return d; });

</script>

<h1>{{ poll.question }}</h1>

<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice_text }} {{ choice.votes }}</li>
{% endfor %}
</ul>
<a href="{% url 'vote' poll.id %}">Vote</a>

Now when you view the results you can see a bar chart which reflects the votes cast.

There is a lot we have glossed over here, but hopefully you can feel the power of Django for defining custom models and create your own web apps around them.