usunyu   Stay hungry. Stay foolish.

Writing your first Django app

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

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;
}

Resource: