The Official Django Tutorial: For the Time-Deprived

I wrote this sorta as notes for myself because I don't like going through the Django tutorial. This is a reduced version of the official 4-part Django tutorial everyone seems to be stuck reading. The Django tutorial's a big pain in the ass because it's too wordy and I some parts are way too hard to understand. The authors made way too many assumptions about the reader after arduously advising they read up on Python before attempting the tutorial. I'm a learn-by-example kinda guy and I always have been, so this reduction is unsurprising. This is still the Poll application the guys at DjangoProject have you write in their version of the tutorial.

If you'd like to skip to the end to see what the resulting code will look like, press Ctrl + F (or the equivalent on your system) and search for "cheat" Running the code you find in that section might be a pain for you if you didn't read the first few parts of this tutorial, as the settings.py configuration (we'll get to that) needs to be set up properly to interact with your code correctly. Therefore, it's probably best you just read through this tutorial from the beginning.

For the record, I recommend you read Part 1 of the official tutorial anyway:

Enjoy.

---Install Django---
This is a Django 1.4 tutorial. It works with 1.5.

To install, you can use git (http://bit.ly/3mWN3O). If you're unfamiliar with git, you can use easy_install or pip. I used git:

git clone django  
cd django  
setup.py install

*If you haven't noticed, those are commands for the git console.

To check the version of Django, you can do the following in the Python shell:

import django  
print django.VERSION

There. All setup?

*Windows users: Make life easier on yourself. Make sure you add the directory in which django is installed to your PATH. By default, that directory should be:

python<version>/lib/site-packages/django/bin/

---Initiate the App--

In the console:

django-admin.py startproject mysite

What's created:

/mysite/  
    manage.py
/mysite/
    __init__.py
    settings.py
    urls.py
    wsgi.py

To test to see if this works:

manage.py runserver

---Set up settings.py---

Database Setup:

DATABASES = {  
    'default': {
        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
    'NAME': 'C:/Documents and Settings/Mommy/mysite/sqldb.db',                      # Or path to database file if using sqlite3.
        'USER': '',                      # Not used with sqlite3.
        'PASSWORD': '',                  # Not used with sqlite3.
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
    }
}
TIME_ZONE = 'US/Eastern'

 

---Sync the DB---

manage.py syncdb

---Start the app---

manage.py startapp polls

What's created:

/polls/  
    __init__.py
    models.py
    tests.py
    views.py

---Create Models---
Models are the data 'pictures.' The app looks at them to see what data is used in the app. They 'model' the behavior of your app.

Example:

from django.db import models

class Poll(models.Model):  
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

class Choice(models.Model):  
    poll = models.ForeignKey(Poll)
    choice = models.CharField(max_length=200)
    votes = models.IntegerField()

Activate the model:

Edit settings.py to include app name:

INSTALLED_APPS = (  
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'polls',
)

To check for errors in your models:

manage.py validate

---Activate the Admin site---
The admin site is for content management.

In INSTALLED_APPS in settings.py, uncomment:

'django.contribu.admin'

Whenever a new app is added, you need the db tables for it to be made. So do:

manage.py syncdb

In urls.py, uncomment:

from django.contrib import admin  
    admin.autodiscover()

    url(r'^admin/', include(admin.site.urls))

Now run the dev server:

manage.py runserver

Go to the admin page:

http://127.0.0.1:8000/admin/

Login using the superuser info created when the db was first sync'd.

Now you have to make the app show in the admin interface. Register the model wanted. Do this by adding an admin.py to the directory of your app. In it should be, for example:

from polls.models import Poll  
from django.contrib import admin

    admin.site.register(Poll)

We need to add some related objects for the purpose of the example being used. In this case, some poll choices need to be added. We created a Choice() model. Now we need to register it, so in admin.py:

from polls.models import Choice  
    admin.site.register(Choice)

---Working with Views---
Views are the displays the user sees, like forms and buttons and whatnot.

First, you need to plan the views. For example:

  • "index" page - displays the available polls
  • "detail" page - displays the question in full with options to vote on
  • "results" page - displays the results of a poll
  • Vote action - this is what processes the choice of the voter

---Design the URLs---
We need to design the paths. To do this, we edit the URL structures. The system works as follows:

  1. User sends a request
  2. Django looks at the ROOT_URLCONF setting in settings.py
  3. Django loads the module-level variable 'urlpatterns'
  4. urlpatterns is a tuple in this format: (regex, callback function [, optional dictionary])
  5. Django compares each regex in the tuple to the requested URL
  6. When Django finds a match, it uses the callback function in an HttpRequest

It's probably a good idea to get out a regex cheatsheet for this(http://bit.ly/skTxNP).

Go to urls.py and edit it to match the views layout. For example:

from django.conf.urls import patterns, include, url  
from django.contrib import admin

admin.autodiscover()

urlpatterns = patterns('',  
    url(r'^polls/$', 'polls.views.index'),
    url(r'^polls/(?P<poll_id>d+)/$', 'polls.views.detail'),
    url(r'^polls/(?P<poll_id>d+)/results/$', 'polls.views.results'),
    url(r'^polls/(?P<poll_id>d+)/vote/$', 'polls.views.vote'),
    url(r'^admin/', include(admin.site.urls)),
)

Explanation of the url() calls:

  1. Django looks at the first parameter, which is a regular expression.
  2. The parameter is compared to the request, which might be something like "/polls/4/"
  3. When a match is found, it loads the function found in the next parameter. In the case of "/polls/4/" that function is "polls.views.detail" because the request adheres to the regex "^polls/ (?P<poll_id>d+) /$"

Internally, the function call triggered by this request looks like this:

detail(request=<HttpRequest object>, poll_id='4')

---Writing Views---
First, check that the urls are valid. To do that, run the server:

manage.py runserver

Try to go to one of the urls, like "/polls/" -- what should show up is a ViewDoesNotExist error.

Second, open polls/views.py. This is where your views will go. The proverbial "Hello, World" can be written as follows:

from django.http import HttpResponse

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

To view that, go to /polls/ again, and you should see that the page was generated rendering the HttpResponse to your HttpRequest (wherein the request was the page you tried to go to).

An example of a series of views to render a response to each request for the purpose of this example project is this:

from django.http import HttpResponse

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

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

def results(request, poll_id):  
    return HttpResponse("You're looking at the results of poll %s." % poll_id)

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

Views have 2 functions:

  1. Handle HttpRequests
  2. Raise exceptions, like 404 errors.

---Working with templates---
In order to stylize our HttpResponse, we need to use templates (or not, but most of the time, since we're having the users interact with Django, we'll need a midium through which the two sectors communicate).

In the case of our example app, we want to display the details of each poll. We also want it stylized, so we need to use some markup (html) with the Django templating system. In views.py should be:

from django.template import Context, loader  
from polls.models import Poll  
from django.http import HttpResponse

def index(request):  
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] #display the latest 5 polls; order them by publish date
    t = loader.get_template('polls/index.html')
    c = Context({
    'latest_poll_list': latest_poll_list,
})
return HttpResponse(t.render(c))

Now, when you go to /polls/ you will see a TemplateDoesNotExist error because HttpResponse(t.render(c)), where 't' is loader.get_template('polls/index.html'), can't find the template file (which is just an html file containing the template-related code segments) 'polls/index.html.' It'd be wise to take note of the function Context(), which takes a dictionary. See: http://bit.ly/MhUQFC for details.

We can shorten that code tremendously by using rendertoresponse():

from django.shortcuts import render_to_response  
from polls.models import Poll

def index(request):  
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})

Now we need the poll details page. The detail() view in views.py will now be coded to include an Http404 response. Therefore, your views.py file should now look like this:

from django.http import HttpResponse  
from django.template import Context, loader  
from polls.models import Poll  
from django.shortcuts import render_to_response, get_object_or_404

def index(request):  
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})

def detail(request, poll_id):  
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': p})

*Note on the Http404 template: To replace the default 404 page, just place a 404.html with Django-templated code in it into the root of your templates directory.

---Reducing urls---
We can drastically reduce the urls.py file to something more readable and less redundant. To do this, change the first parameter in the first urlpatterns declaration to the commonality between the function calls. In the case of our polls application, that commonality is clearly 'polls.views' Afterward, you can just reduce each of the following function call parameters that once suited the now-first parameter to just the model function names. Therefore, your new urls.py should look like this:

from django.conf.urls import patterns, include, url  
from django.contrib import admin

admin.autodiscover()

urlpatterns = patterns('polls.views',  
    url(r'^polls/$', 'index'),
    url(r'^polls/(?P<poll_id>d+)/$', 'detail'),
    url(r'^polls/(?P<poll_id>d+)/results/$', 'results'),
    url(r'^polls/(?P<poll_id>d+)/vote/$', 'vote'),
)

urlpatterns += patterns('',  
    url(r'^admin/', include(admin.site.urls)),
)

We can further reduce that file to something more simple by making a URLSconf (just another urls.py) file specific to the app (so within the app directory). So:
create a urls.py file in the polls/ folder

Fill the file with this:

from django.conf.urls import patterns, include, url

urlpatterns = patterns('polls.views',  
    url(r'^$', 'index'),
    url(r'^(?P<poll_id>d+)/$', 'detail'),
    url(r'^(?P<poll_id>d+)/results/$', 'results'),
    url(r'^(?P<poll_id>d+)/vote/$', 'vote'),
)

Now, replace the contents of your urls.py file in your mysite/ folder with:

from django.conf.urls import patterns, include, url  
from django.contrib import admin

admin.autodiscover()

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

Just note that include() here does exactly what it looks like it does. There's no mystery. The point of taking this step is this: When you have multiple projects and want to include different URL schemes in that project, you do this. Look at the documentation for some more details. I'd look at some forums and whatnot on the subject though.

---Writing your first Form---
Edit the detail.html file so that it looks like this:

<h1>{{ poll.question }}</h1>

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

<form action="/polls/{{ poll.id }}/vote/" method="post">
{% csrf_token %}
{% for choice in poll.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

Everything looks considerably readable. The only real unidentified line is the one with csrf_token in it. CSRF stands for Cross-Site Request Forgery. A csrf is a bad thing. Therefore, in order to protect your POST forms (as with this one), you throw in the line:

{% csrf_token %}

Now modify the detail() view in views.py in your polls/ folder to suit the change. Your views.py should now look like this:

from django.http import HttpResponse  
from django.template import Context, loader  
from polls.models import Poll  
from django.template import RequestContext  
from django.shortcuts import render_to_response, get_object_or_404

def index(request):  
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('C:/Documents and Settings/Mommy/mysite/templates/index.html', {'latest_poll_list': latest_poll_list})

def detail(request, poll_id):  
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': p},
    context_instance=RequestContext(request))

RequestContext was used in the rendertoresponse() call because the CSRF token requires a context object.

We can edit the vote() view now so that it handles the vote data; this is what happens when the 'vote' button is pressed. Your views.py file should be modified to look like this:

from django.shortcuts import get_object_or_404, render_to_response  
from django.http import HttpResponseRedirect, HttpResponse  
from django.core.urlresolvers import reverse  
from django.template import RequestContext  
from polls.models import Choice, Poll

def index(request):  
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('C:/Documents and Settings/Mommy/mysite/templates/index.html', {'latest_poll_list': latest_poll_list})

def detail(request, poll_id):  
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': p},
    context_instance=RequestContext(request))

def results(request, poll_id):  
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/results.html', {'poll': p})

def vote(request, poll_id):  
    p = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
    # Redisplay the poll voting form.
        return render_to_response('polls/detail.html', {
            'poll': p,
            'error_message': "You didn't select a choice.",
        }, context_instance=RequestContext(request))
    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.views.results', args=(p.id,)))

You can replace the results() view now with the following:

def results(request, poll_id):  
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/results.html', {'poll': p})

Now create a results.html. Here's what it should look like (for the purpose of this tutorial):

<h1>{{ poll.question }}</h1>

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

<a href="/polls/{{ poll.id }}/">Vote again?</a>

You have a working app now. Congrats. Okay, but there's an easier way to go about this. I'd have skipped the above if I wasn't merely reducing the official DjangoProject tutorial. Now we can use what makes Django extremely powerful and useful: Generic Views.

---An Introduction to Generic Views---
Generic Views reduces code dramatically. Consider how long the views.py code is. It's pretty long considering our app's function is negligible.

For this part, we're just going to do the following to our modules:

  1. Change the URL configurations to suit the generic views (the urls.py files)
  2. Delete unnecessary views

First we need to know which generic views we're going to use. We're going to use the following:

  1. ListView (http://bit.ly/LOTUdm) - According to the site, this is generally used to list a number of objects
  2. DetailView (http://bit.ly/S5dlCk)- We use this when we want to display the details of a particular object (in this case, we have multiple objects)

Additionally, as stated in the official tutorial: "We've added a name, poll_results, to the results view so that we have a way to refer to its URL later on."

First, go to polls/urls.py and change it so it looks like this:

from django.conf.urls import patterns, include, url  
from django.views.generic import DetailView, ListView  
from polls.models import Poll

urlpatterns = patterns('',  
    url(r'^$',
        ListView.as_view(
            queryset=Poll.objects.order_by('-pub_date')[:5],
            context_object_name='latest_poll_list',
            template_name='polls/index.html')),
    url(r'^(?P<pk>\d+)/$',
        DetailView.as_view(
            model=Poll,
            template_name='polls/detail.html')),
    url(r'^(?P<pk>\d+)/results/$',
        DetailView.as_view(
            model=Poll,
            template_name='polls/results.html'),
            name='poll_results'),
    url(r'^(?P<poll_id>\d+)/vote/$', 'polls.views.vote'),
)

Now, go to your views.py file in polls/ and do the following:

  1. Delete results()
  2. Delete detail()
  3. Delete index()

Finally, change the vote() view to suit the Generic View url patterns by replacing the last line in your vote() view with:
return HttpResponseRedirect(reverse('poll_results', args=(p.id,)))

That's it! You're done! I hope this simplified things for you. I really hated going through that tutorial more than once.

For convenience, the following are the files you should have in the end.

 ---cheat ---

mysite/mysite/urls.py:

from django.conf.urls import patterns, include, url  
from django.contrib import admin

admin.autodiscover()

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

mysite/polls/admin.py:

from polls.models import Poll  
from polls.models import Choice  
from django.contrib import admin

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

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

    admin.site.register(Poll, PollAdmin)

mysite/polls/models.py:

from django.db import models  
import datetime  
from django.utils import timezone

# Create your models here.

class Poll(models.Model):  #inherit from the models class  
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __unicode__(self):
        return self.question

    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

class Choice(models.Model):  
    poll = models.ForeignKey(Poll)
    choice = models.CharField(max_length=200)
    votes = models.IntegerField()

    def __unicode__(self):
        return self.choice

mysite/polls/urls.py:

from django.conf.urls import patterns, include, url  
from django.views.generic import DetailView, ListView  
from polls.models import Poll

urlpatterns = patterns('',  
    url(r'^$',
        ListView.as_view(
            queryset=Poll.objects.order_by('-pub_date')[:5],
            context_object_name='latest_poll_list',
            template_name='C:/Documents and Settings/Mommy/mysite/templates/index.html')),
    url(r'^(?P<pk>\d+)/$',
        DetailView.as_view(
            model=Poll,
            template_name='C:/Documents and Settings/Mommy/mysite/templates/detail.html')),
    url(r'^(?P<pk>\d+)/results/$',
        DetailView.as_view(
            model=Poll,
            template_name='C:/Documents and Settings/Mommy/mysite/templates/results.html'),
            name='poll_results'),
    url(r'^(?P<poll_id>\d+)/vote/$', 'polls.views.vote'),
)

mysite/polls/views.py:

from django.shortcuts import get_object_or_404, render_to_response  
from django.shortcuts import get_object_or_404, render_to_response  
from django.http import HttpResponseRedirect, HttpResponse  
from django.core.urlresolvers import reverse  
from django.template import RequestContext  
from polls.models import Choice, Poll

def vote(request, poll_id):  
    p = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the poll voting form.
    return render_to_response('C:/Documents and Settings/Mommy/mysite/templates/detail.html', {
            'poll': p,
            'error_message': "You didn't select a choice.",
        }, context_instance=RequestContext(request))
    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('poll_results', args=(p.id,)))

mysite/templates/index.html:

{% if latest_poll_list %}  
<ul>
{% for poll in latest_poll_list %}
    <li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
{% endfor %}
</ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

mysite/templates/detail.html:

<h1>{{ poll.question }}</h1>

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

<form action="/polls/{{ poll.id }}/vote/" method="post">
{% csrf_token %}
{% for choice in poll.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

mysite/templates/results.html:

<h1>{{ poll.question }}</h1>

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

<a href="/polls/{{ poll.id }}/">Vote again?</a>

 

Greg

Software Engineer

Subscribe to GregBlogs