An Introduction to Django – Part 3

In Part 3 of this introduction to Django series, we’ll look at making a simple form and also making views more generic.

Make a Simple Form

Lets put the form in the detail template.  Open the detail template (templates/polls/detail.html).  Change the file so that it looks like this:

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

{% if error_message %}<p><strong>{{ error_message }}</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>

This looks a lot worse than it really is.  Inside the form, there is a for loop that loops over each choice in the question.  It displays each choice as a radio button (this is the <input type = “radio” …> line).  When somebody submits the form, it will send the POST data choice = # where the # is the ID of the selected choice.

Now, we need to update our view so that it can handle the submitted data and do something with it.  Open polls/views.py to edit your views.

Add the following to the views file:

from django.http import HttpResponseRedirect

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,)))

Here, we use request.POST[‘choice’] to grab the question ID of the selected choice.  The reverse() function within HttpResponseRedirect() lets us avoid hardcoding in a URL.  The inputs here are a view name and the variable portion of the URL pattern for that view.

This view redirects a user to the results page after they submit a vote.  To make that view, add the following to 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})

And now we need to create this template.  Create a file /templates/polls/results.html that has the following in it:

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

Now navigate to http://localhost:8000/polls/1/ and vote.  You should be redirected to the results page where it shows the total votes.  If you try to vote without making a choice, you should receive an error.

At this point, our simple polling app has all of the base functionality to work as we want.  Now, lets go over the views and see if we can clean up the code by making them more generic.

Use Generic Views

As with any other kind of programming, it is a good idea to generalize something that will be used more than once (if possible).  The same is of course true for views.

If we look at our views, we can see that the detail and results views are really similar.  Keeping them as is would be redundant, so lets make one view that we can use for both.

Luckily, this is so common that Django has provided a shortcut called the ‘generic views system’.  To use this, we will need to follow two steps:

1 Convert the URLconf

Open polls/urls.py and change it to look like this:

from django.conf.urls import url

from . import views

app_name = 'polls'
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'),
]

In addition to adding .__View to the views object, we also changed the <question_id> to simply <pk> to make this be more generic.

2 Amend The Views

Open the polls/views.py file.  We need to remove the old indexdetail, and results views and use our new generic view instead.

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

Notice that we changed these from functions (using def) to classes (using class).  Next, notice that we are using two different generic views (ListView and DetailView).  The ListView is a generic view for displaying a list of objects and DetailView is a generic view for displaying a detail page for a type of object.

Each generic view needs to know what model it will act on, using the model attribute.  Also, the DetailView expects a primary key value from the URL to be called pk, which is why we changed the question_id to pk in the URLconf.

The DetailView generic view uses a template by default called <app name>/<model name>_detail.html.  For us, that will be polls/question_detail.html.  If you don’t want to use this default one, you can specify which template to use by using the template_name variable, which we do in each of the classes.

That’s it for this portion.  Django is still very difficult for me to grasp.  I feel like if I were trying to do my own app, I wouldn’t know all of these details (like template_name and context_object_name).  I haven’t checked the documentation, but I’m hopeful that it is good enough that when I’m ready to do my own app, I’ll feel more confident.

Next time, we’ll look at automated tests, what they are, why they are good, and how to use them.

Have questions or suggestions?  Please feel free to comment below or contact me.

Leave a Reply

Your email address will not be published. Required fields are marked *