My first Django app
This is my implementaion on the Django tutorial.
Create Django project and a polls
app
First, I verify my Django installation and version by:
python -m django --version
If Python (use 3 here) or Django (1.11.23 here) is not installed, go by:
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
.
django-admin startproject mysite
And this creates:
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)
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:
python manage.py startapp polls
And this creates:
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:
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
:
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
:
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:
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
:
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
:
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:
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.
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:
python manage.py makemigrations polls
I could also see the SQL that migration would run by:
python manage.py sqlmigrate polls 0001
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:
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
:
$ 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:
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:
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.
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)
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:
{% 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
.
# ...
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
.
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
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.
<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.
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.
<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>
# ...
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:
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:
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.
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
.
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
.
{% 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
:
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:
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/
:
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:
{% 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:
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.