This is my implementaion on the Django tutorial.

Create Django project and a polls app

First, I verify my Django installation and version by:

1
python -m django --version

If Python (use 3 here) or Django (1.11.23 here) is not installed, go by:

1
2
3
4
5
6
brew install python
mkdir ~/.venvs/venv
python3 -m venv ~/.venvs/venv
source ~/.venvs/venv/bin/activate
pip install django==1.11.23
pip install psycopg2

Now I could create a project with a collection of settings for an instance of Django. Avoid naming projects after built-in Python (test) or Django (django) components. And for security, put codes in some directory outside of the document root, such as /home/mycode.

1
django-admin startproject mysite

And this creates:

1
2
3
4
5
6
7
mysite
├── manage.py # a command-line utility
└── mysite # actual Python package for mysite project
    ├── __init__.py
    ├── settings.py # settings/configuration
    ├── urls.py # URL declaration; a 'table of contents' of the Django-powered site
    └── wsgi.py # entry-point for WSGI-compatible web servers to serve the project

I could run and verify the function of the development server (default port is 8000)

1
python manage.py runserver 8001

The development server automatically reloads Python code for each request as needed.

Now I have a project (a collection of configuration), I want to create some apps for the website right next to my manage.py as a top-level module. So I create the Polls app by:

1
python manage.py startapp polls

And this creates:

1
2
3
4
5
6
7
8
9
polls
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

Write my first view for the polls app

Edit polls/views.py as:

1
2
3
4
5
from django.shortcuts import render
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

Map the view it to a URL by URLconf polls/urls.py:

1
2
3
4
5
6
7
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
]

And point the root URLconf at the polls.urls module by editing mysite/urls.py:

1
2
3
4
5
6
7
from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^polls/', include('polls.urls')),
    url(r'^admin/', admin.site.urls),
]

The include() function allows referencing other URLconfs. You should always use include() when you include other URL patterns. admin.site.urls is the only exception to this.

The url(regex, view, [kwargs, name]) function is passed four arguments. Note that the regex does not search GET and POST parameters, or the domain name.

Setup database for Django apps

Prepare PostgreSQL database:

1
2
3
4
5
6
brew install postgresql@10
brew link postgresql@10 --force
brew services start postgresql@10
createuser --superuser djangouser
createdb -O djangouser djangodb
psql djangodb; ALTER USER djangouser PASSWORD 'mypassword';

Setup database in mysite/settings.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'djangodb',
        'USER': 'djangouser',
        'PASSWORD': 'mypassword',
        'HOST': '127.0.0.1',
        'PORT': '5432'
    }
}

Then run migrate command to create the tables for INSTALLED_APPS in the database, currently for admin, auth, contenttypes and sessions:

1
python manage.py migrate

Create Models for the polls app

Change models in models.py

Now I will define my models, essentially my database layout with additional metadata. Note Django follows the DRY (Don’t Repeat Yourself, every piece of knowledge must have a single, unambiguous, authoritative representation within a system) principle.

I create two models Questions and Choice in my simple poll app:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from django.db import models
from django.utils.encoding import python_2_unicode_compatible

@python_2_unicode_compatible # support Python 2
class Question(models.Model):
    question_text = models.CharField(max_length=200) # field
    pub_date = models.DateTimeField('date published')

    # it is important to add object representation
    # especially for Django's automatically-generated admin
    def __str__(self):
        return self.question_text

    # custom method for demonstration
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

    # custom the question change list in admin view
    was_published_recently.admin_order_field = 'pub_date'
    was_published_recently.boolean = True # use icon to represent true or false
    was_published_recently.short_description = 'Published recently?'

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE) # relationship
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
    def __str__(self):
        return self.choice_text

Then I could add a reference of polls app to its configuration class in the INSTALLED_APPS setting.

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Run python manage.py makemigrations to create migrations for those changes

Now run the command to tell Django that I would like store the changes for polls as a migration:

1
python manage.py makemigrations polls

I could also see the SQL that migration would run by:

1
python manage.py sqlmigrate polls 0001
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_choice" ("id" serial NOT NULL PRIMARY KEY, "choice_text" varchar(200
) NOT NULL, "votes" integer NOT NULL);
--
-- Create model Question
--
CREATE TABLE "polls_question" ("id" serial NOT NULL PRIMARY KEY, "question_text" varchar
(200) NOT NULL, "pub_date" timestamp with time zone NOT NULL);
--
-- Add field question to choice
--
ALTER TABLE "polls_choice" ADD COLUMN "question_id" integer NOT NULL;
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
ALTER TABLE "polls_choice" ADD CONSTRAINT "polls_choice_question_id_c5b4b260_fk_polls_qu
estion_id" FOREIGN KEY ("question_id") REFERENCES "polls_question" ("id") DEFERRABLE INI
TIALLY DEFERRED;
COMMIT;

Run python manage.py migrate to apply those changes to the database

Then I create those model tables in my database by running migrate again:

1
python manage.py migrate

Use Django APIs to interact with the polls app

Now I could play with the Django API using the Python shell under DJANGO_SETTINGS_MODULE environment variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ python manage.py shell
>>> from polls.models import Question, Choice
>>> Question.objects.all()
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
>>> q.save()
>>> q.id
>>> q.question_text
>>> q.pub_date
>>> Question.objects.get(pub_date__year=timezone.now().year)
>>> Question.objects.get(id=1) # identical to Question.objects.get(pk=1)
>>> q.was_published_recently()

Use Django Admin to manage the Question object

Django was written in a newsroom environment. The admin is intended to be used for site managers to edit content.

An admin could be created by:

1
python manage.py createsuperuser

Then I could login through http://127.0.0.1:8001/admin/

Don’t forget to register the Question object with an admin interface by:

1
2
3
4
from django.contrib import admin
from .models import Question

admin.site.register(Question)

Four public interface views for the polls app

I will create four views for the polls app:

  • Question “index” page: displays the latest few questions.
  • Question “detail” page: displays a question text, with no results but with a form to vote.
  • Question “results” page: displays results for a particular question.
  • Vote action: handles voting for a particular choice in a particular question.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.shortcuts import render
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.conf.urls import url

from . import views

app_name = 'polls'
urlpatterns = [
    # e.g. /polls/
    url(r'^$', views.index, name='index'),
    # e.g. /polls/5/
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    # e.g. /polls/5/results/
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    # e.g. /polls/5/vote/
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

Index view

I could use templates directory to seperate design from Python:

1
2
3
4
5
6
7
8
9
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Here I use <a href="{% url 'polls:detail' question.id %}"> to avoid hardcoded link like <a href="/polls/{{ question.id }}/">, so that it look up the URL definition as specified in the polls.url module. I use polls:detail here to point at the namespaced detail view, as 'polls' is specified as app_name in polls/urls.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ...
from django.utils import timezone
from .models import Question

def index(request):
    latest_question_list = Question.objects.filter(
        pub_date__lte=timezone.now()
    ).order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)
# ...

Here I use render() shortcut for HttpResponse(loader.get_template(file).render(context, request)).

Detail view or raise a 404 error

Each view is responsible for doing one of two things: returning an HttpResponse object containing the content for the requested page, or raising an exception such as Http404.

1
2
3
4
5
6
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

1
2
3
4
5
6
7
8
9
from django.shortcuts import get_object_or_404, render
from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(
        Question.objects.filter(pub_date__lte=timezone.now()), pk=question_id
    )
    return render(request, 'polls/detail.html', {'question': question})
# ...

Here I use get_object_or_404() shortcut for Http404 exception.

Vote view: creating a simple form

Now I update my poll detail template and add a simple form to it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<h1>{{ question.question_text }}</h1>

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

<!--POST, act of submitting form will alter data server-side-->
<form action="{% url 'polls:vote' question.id %}" method="post">
  <!--this protect from Cross Site Request Forgeries-->
  {% csrf_token %}
  {% for choice in question.choice_set.all %}
  <!--display a radio button which POST data choice=choice.id-->
  <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>

Note all POST forms that are targeted at internal URLs should use the {% csrf_token %} template tag to protect from Cross Site Request Forgeries.

And I also replace the dummy implementation of the vote() function with a real one.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            '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 HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Note that always return an HttpResponseRedirect after successfully dealing with POST data to prevent from posting data twice if a user hits the Back button. And I use the reverse() function in the HttpResponseRedirect constructor to help avoid hardcoding a URL in the view function.

And note that this vote() have a race condition problem: if two users of my website try to vote at exactly the same time, the computed new value of votes might go wrong.

Result view

Now I implement the result() view by creating a result template and update the view.

1
2
3
4
5
6
7
8
9
<h1>{{ question.question_text }}</h1>

<ul>
  {% for choice in question.choice_set.all %}
  <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
  {% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

1
2
3
4
5
6
7
# ...
def results(request, question_id):
    question = get_object_or_404(
        Question.objects.filter(pub_date__lte=timezone.now), pk=question_id
    )
    return render(request, 'polls/results.html', {'question': question})
# ...

Use Django’s generic views instead for some simple views in the polls app

Since it is pretty common for basic Web development to get data from the database according a passing parameter in the URL, loading a template and returning template, Django provides generic views for the simple detail(), results() and index() views.

In practice, when writing a Django app, people evaluate whether generic views are a good fit for their problem, and use them from the beginning.

I amend the polls/urls.py URLconf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.conf.urls import url

from . import views

app_name = 'polls'
urlpatterns = [
    # e.g. /polls/
    url(r'^$', views.IndexView.as_view(), name='index'),
    # e.g. /polls/5/
    url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
    # e.g. /polls/5/results/
    url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
    # e.g. /polls/5/vote/
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

Note <question_id> in second and third patterns changes to <pk>.

I also amend index(), detail() and results() views with Django’s generic views:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic
from django.utils import timezone

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now())


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'

    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now())

# ...

Automated testing

I will create some automated tests so that as I make changes to my app, I could check that my code still works well. Besides,

  • Tests will save time
  • Tests don’t just identify problems, they prevent them
  • Tests make my code more attractive
  • Tests help teams work together

Rules of thumb:

  • a separate TestClass for each model or view
  • a separate test method for each set of conditions you want to test
  • test method names that describe their function

Here is some tests to check if the polls app not deals with future questions.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import datetime

from django.utils import timezone
from django.test import TestCase
from django.urls import reverse

from .models import Question


class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)

    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() returns True for questions whose pub_date
        is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)
        self.assertIs(recent_question.was_published_recently(), True)


def create_question(question_text, days):
    """
    Create a question with the given `question_text` and published the
    given number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)


class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
        If no questions exist, an appropriate message is displayed.
        """
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_past_question(self):
        """
        Questions with a pub_date in the past are displayed on the
        index page.
        """
        create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    def test_future_question(self):
        """
        Questions with a pub_date in the future aren't displayed on
        the index page.
        """
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_future_question_and_past_question(self):
        """
        Even if both past and future questions exist, only past questions
        are displayed.
        """
        create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    def test_two_past_questions(self):
        """
        The questions index page may display multiple questions.
        """
        create_question(question_text="Past question 1.", days=-30)
        create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question 2.>', '<Question: Past question 1.>']
        )


class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text='Future question.', days=5)
        url = reverse('polls:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text='Past Question.', days=-5)
        url = reverse('polls:detail', args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

Customize the polls app’s look and feel

Django will look for statis files in the polls/static/ directory. Its STATICFILES_FINDERS setting contains a list of finders that know how to discover static files from various source. AppDirectoriesFinder staticfile finder finds the stylesheet at polls/static/polls/style.css.

1
2
3
4
5
6
7
li a {
    color: green;
}

body {
    background: white url("images/background.gif") no-repeat right bottom;
}

And I add the {% static %} template tag at the top of polls/templates/polls/index.html.

1
2
3
{% load static %}

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">

Note always use relative paths to link static files between each other.

Customize the admin site

I customize the admin form by editing polls/admin.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from django.contrib import admin

from .models import Choice, Question


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


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]
    list_display = ('question_text', 'pub_date', 'was_published_recently')
    list_filter = ['pub_date']
    search_fields = ['question_text']

admin.site.register(Question, QuestionAdmin)

I use fieldsets to split the form up and provide an usability detail. Alternatively, fields could be directly used here serving only a few fields.

For ChoiceInline, with TabularInline, the related objects are displayed in a compact, table-based format. I could also spread it out using StackInline.

I can also customize the Django admin itself. First I add a DIRS option in the TEMPLATES setting in the setting files:

1
2
3
4
5
6
7
TEMPLATES = [
    {
        # ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        # ...
    },
]

Find the Django source file, and copy the django/contrib/admin/templates/admin/base_site.html into templates/admin/:

1
2
3
python -c "import django; print(django.__path__)"
mkdir templates/admin
cp PATH/django/contrib/admin/templates/admin/base_site.html templates/admin/base_site.html

Edit as:

1
2
3
4
5
6
7
8
9
{% extends "admin/base.html" %}

{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}

{% block nav-global %}{% endblock %}

Wrap up

Now my Django app looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mysite
├── db.sqlite3
├── manage.py
├── mysite
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── polls
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   ├── __init__.py
│   ├── models.py
│   ├── static
│   │   └── polls
│   │       ├── images
│   │       │   └── background.gif
│   │       └── style.css
│   ├── templates
│   │   └── polls
│   │       ├── detail.html
│   │       ├── index.html
│   │       └── results.html
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── templates
    └── admin
        └── base_site.html

It is also useful to turn the polls app into a standalone Python package, but it is another story.