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