Writing your first Django app
10 Dec 20141. Throughout this tutorial, we’ll walk you through the creation of a basic poll application.
Check Django is installed and which version (Setup Django):
$ python -c "import django; print(django.get_version())"
Creating a project:
$ django-admin startproject mysite
Database setup:
Edit mysite/settings.py
:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'mysite',
'USER': 'root',
'PASSWORD': '',
'HOST': 'localhost',
'PORT': '3306',
}
}
$ python manage.py migrate
The development server:
$ python manage.py runserver
Changing the port:
$ python manage.py runserver 8080
$ python manage.py runserver 0.0.0.0:8000
Creating models:
$ python manage.py startapp polls
Edit the polls/models.py
:
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
class Choice(models.Model):
question = models.ForeignKey(Question)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
Activating models:
Edit the mysite/settings.py
file again:
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'polls',
)
$ python manage.py makemigrations polls
$ python manage.py sqlmigrate polls 0001
$ python manage.py migrate
Playing with the API:
$ 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
>>> q.question_text = "What's up?"
>>> q.save()
>>> Question.objects.all()
Edit polls/models.py
, add functions:
import datetime
from django.db import models
from django.utils import timezone
class Question(models.Model):
# ...
def __str__(self): # __unicode__ on Python 2
return self.question_text
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
class Choice(models.Model):
# ...
def __str__(self): # __unicode__ on Python 2
return self.choice_text
Run python manage.py shell
again:
>>> from polls.models import Question, Choice
>>> Question.objects.all()
>>> Question.objects.filter(id=1)
>>> Question.objects.filter(question_text__startswith='What')
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
>>> Question.objects.get(id=2)
>>> Question.objects.get(pk=1)
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
>>> q = Question.objects.get(pk=1)
>>> q.choice_set.all()
>>> q.choice_set.create(choice_text='Not much', votes=0)
>>> q.choice_set.create(choice_text='The sky', votes=0)
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
>>> c.question
>>> q.choice_set.all()
>>> q.choice_set.count()
>>> Choice.objects.filter(question__pub_date__year=current_year)
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()
2. We’re continuing the Web-poll application and will focus on Django’s automatically-generated admin site.
Creating an admin user:
$ python manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password: **********
Password (again): *********
Superuser created successfully.
Start the development server:
$ python manage.py runserver
Open http://127.0.0.1:8000/admin/
Make the poll app modifiable in the admin:
polls/admin.py
:
from django.contrib import admin
from polls.models import Question
admin.site.register(Question)
Customize the admin form:
polls/admin.py
:
from django.contrib import admin
from polls.models import Question
// admin.site.register(Question)
class QuestionAdmin(admin.ModelAdmin):
fields = ['pub_date', 'question_text']
// or
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
admin.site.register(Question, QuestionAdmin)
Adding related objects:
polls/admin.py
:
from django.contrib import admin
from polls.models import Choice, Question
# ...
admin.site.register(Choice)
// or
from django.contrib import admin
from polls.models import Choice, Question
class ChoiceInline(admin.StackedInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
inlines = [ChoiceInline]
admin.site.register(Question, QuestionAdmin)
Customize the admin change list:
polls/admin.py
:
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date', 'was_published_recently')
list_filter = ['pub_date']
search_fields = ['question_text']
Customize the admin look and feel:
mysite/settings.py
:
TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]
$ python -c "
import sys
sys.path = sys.path[1:]
import django
print(django.__path__)"
$ cp /Library/Python/2.7/site-packages/django/contrib/admin/templates/admin/base_site.html ~/mysite/templates/admin
Replace { { site_header|default:_('Django administration') } }
in base_site.html
:
<h1 id="site-name"><a href="{ % url 'admin:index' % }">Polls Administration</a></h1>
3. We’re continuing the Web-poll application and will focus on creating the public interface – “views.”
Write your first view:
Open the file polls/views.py
:
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
Create a file called urls.py
in polls app, polls/urls.py
:
from django.conf.urls import url
from polls import views
urlpatterns = [
url(r'^$', views.index, name='index'),
]
Writing more views:
polls/views.py
:
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
polls.urls
:
from django.conf.urls import url
from polls import views
urlpatterns = [
# ex: /polls/
url(r'^$', views.index, name='index'),
# ex: /polls/5/
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
# ex: /polls/5/results/
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
# ex: /polls/5/vote/
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
Write views that actually do something:
polls/views.py
:
from django.http import HttpResponse
from polls.models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
output = ', '.join([p.question_text for p in latest_question_list])
return HttpResponse(output)
Create a file called templates/polls/index.html
in your polls
directory, polls/templates/polls/index.html
:
{ % if latest_question_list % }
<ul>
{ % for question in latest_question_list % }
<li><a href="/polls/{ { question.id } }/">{ { question.question_text } }</a></li>
{ % endfor % }
</ul>
{ % else % }
<p>No polls are available.</p>
{ % endif % }
polls/views.py
:
from django.http import HttpResponse
from django.template import RequestContext, loader
from polls.models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template('polls/index.html')
context = RequestContext(request, {
'latest_question_list': latest_question_list,
})
return HttpResponse(template.render(context))
// or
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)
Raising a 404 error:
polls/views.py
:
from django.http import Http404
from django.shortcuts import render
from polls.models import Question
# ...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404
return render(request, 'polls/detail.html', {'question': question})
polls/templates/polls/detail.html
:
{ { question } }
A shortcut: get_object_or_404()
, polls/views.py
:
from django.shortcuts import get_object_or_404, render
from polls.models import Question
# ...
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
Use the template system:
polls/templates/polls/detail.html
:
<h1>{ { question.question_text } }</h1>
<ul>
{ % for choice in question.choice_set.all % }
<li>{ { choice.choice_text } }</li>
{ % endfor % }
</ul>
Removing hardcoded URLs in templates:
polls/templates/polls/index.html
:
<li><a href="/polls/{ { question.id } }/">{ { question.question_text } }</a></li>
<!-- to -->
<li><a href="{ % url 'detail' question.id % }">{ { question.question_text } }</a></li>
Namespacing URL names:
mysite/urls.py
:
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^polls/', include('polls.urls', namespace="polls")),
url(r'^admin/', include(admin.site.urls)),
]
polls/templates/polls/index.html
:
<li><a href="{ % url 'detail' question.id % }">{ { question.question_text } }</a></li>
<!-- to -->
<li><a href="{ % url 'polls:detail' question.id % }">{ { question.question_text } }</a></li>
4. We’re continuing the Web-poll application and will focus on simple form processing and cutting down our code.
Write a simple form:
polls/templates/polls/detail.html
:
<h1>{ { question.question_text } }</h1>
{ % if error_message % }<p><strong></strong></p>{ % endif % }
<form action="{ % url 'polls:vote' question.id % }" method="post">
{ % csrf_token % }
{ % for choice in question.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>
polls/views.py
:
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
from polls.models import Choice, Question
# ...
def vote(request, question_id):
p = get_object_or_404(Question, pk=question_id)
try:
selected_choice = p.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {
'question': p,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:results', args=(p.id,)))
polls/views.py
:
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
Create a polls/results.html
template:
<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>
Amend URLconf:
First, open the polls/urls.py
URLconf and change it like so:
from django.conf.urls import url
from polls import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
Amend views:
Next, we’re going to remove our old index, detail, and results views and use Django’s generic views instead. To do so, open the polls/views.py
file and change it like so:
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views import generic
from polls.models import Choice, Question
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
def vote(request, question_id):
... # same as above
5. We’ve built a Web-poll application, and we’ll now create some automated tests for it.
Create a test to expose the bug:
Put the following in the tests.py
file in the polls application:
import datetime
from django.utils import timezone
from django.test import TestCase
from polls.models import Question
class QuestionMethodTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is in the future
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertEqual(future_question.was_published_recently(), False)
Running tests:
$ python manage.py test polls
Fixing the bug:
polls/models.py
:
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
More comprehensive tests:
polls/tests.py
:
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is older than 1 day
"""
time = timezone.now() - datetime.timedelta(days=30)
old_question = Question(pub_date=time)
self.assertEqual(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() should return True for questions whose
pub_date is within the last day
"""
time = timezone.now() - datetime.timedelta(hours=1)
recent_question = Question(pub_date=time)
self.assertEqual(recent_question.was_published_recently(), True)
$ python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test import Client
>>> client = Client()
>>> response = client.get('/')
>>> response.status_code
>>> from django.core.urlresolvers import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
>>> response.content
>>> from polls.models import Question
>>> from django.utils import timezone
>>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now())
>>> q.save()
>>> response = client.get('/polls/')
>>> response.content
>>> response.context['latest_question_list']
Improving our view:
polls/views.py
:
from django.utils import timezone
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]
Testing our new view:
Add the following to polls/tests.py
:
from django.core.urlresolvers import reverse
def create_question(question_text, days):
"""
Creates a question with the given `question_text` 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 QuestionViewTests(TestCase):
def test_index_view_with_no_questions(self):
"""
If no questions exist, an appropriate message should be 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_index_view_with_a_past_question(self):
"""
Questions with a pub_date in the past should be 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_index_view_with_a_future_question(self):
"""
Questions with a pub_date in the future should not be 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.",
status_code=200)
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_index_view_with_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
should be 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_index_view_with_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.>']
)
Testing the DetailView:
polls/views.py
:
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
polls/tests.py
:
class QuestionIndexDetailTests(TestCase):
def test_detail_view_with_a_future_question(self):
"""
The detail view of a question with a pub_date in the future should
return a 404 not found.
"""
future_question = create_question(question_text='Future question.', days=5)
response = self.client.get(reverse('polls:detail', args=(future_question.id,)))
self.assertEqual(response.status_code, 404)
def test_detail_view_with_a_past_question(self):
"""
The detail view of a question with a pub_date in the past should
display the question's text.
"""
past_question = create_question(question_text='Past Question.', days=-5)
response = self.client.get(reverse('polls:detail', args=(past_question.id,)))
self.assertContains(response, past_question.question_text, status_code=200)
6. We’ve built a tested Web-poll application, and we’ll now add a stylesheet and an image.
Customize your app’s look and feel
Put the following code in that stylesheet (polls/static/polls/style.css
):
li a {
color: green;
}
Next, add the following at the top of polls/templates/polls/index.html
:
{ % load staticfiles % }
<link rel="stylesheet" type="text/css" href="{ % static 'polls/style.css' % }" />
Adding a background-image:
polls/static/polls/style.css
:
body {
background: white url("images/background.gif") no-repeat right bottom;
}