Tuesday, June 19, 2007

Focus on creating the public interface — “views”

Philosophy

A view is a “type” of Web page in your Django application that generally serves a specific function and has a specific template. For example, in a weblog application, you might have the following views:

  • Blog homepage — displays the latest few entries.
  • Entry “detail” page — permalink page for a single entry.
  • Year-based archive page — displays all months with entries in the given year.
  • Month-based archive page — displays all days with entries in the given month.
  • Day-based archive page — displays all entries in the given day.
  • Comment action — handles posting comments to a given entry.

In our poll application, we’ll have the following four views:

  • Poll “archive” page — displays the latest few polls.
  • Poll “detail” page — displays a poll question, with no results but with a form to vote.
  • Poll “results” page — displays results for a particular poll.
  • Vote action — handles voting for a particular choice in a particular poll.

In Django, each view is represented by a simple Python function.

Design your URLs

format:
(regular expression, Python callback function [, optional dictionary])

Default URLconf in mysite/urls.py. It also automatically set your ROOT_URLCONF setting (in settings.py) to point at that file:

ROOT_URLCONF = 'mysite.urls'

Time for an example. Edit mysite/urls.py so it looks like this:

from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^polls/$', 'mysite.polls.views.index'),
(r'^polls/(?P\d+)/$', 'mysite.polls.views.detail'), (r'^polls/(?P\d+)/results/$', 'mysite.polls.views.results'), (r'^polls/(?P\d+)/vote/$', 'mysite.polls.views.vote'),
)
The poll_id='23' part comes from (?P\d+). Using parenthesis around a
pattern “captures” the text matched by that pattern and sends it as an argument
to the view function; the ?P defines the name that will be used to
identify the matched pattern; and \d+ is a regular expression to match a sequence of
digits (i.e., a number).

function call with parameter like this: detail(request=, poll_id='23')


Write view

Open the file mysite/app_dir/views.py and put the following Python code in it:

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)


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. The rest is up to you.

Your view can read records from a database, or not. It can use a template system
such as Django’s — or a third-party Python template system — or not.

It can generate a PDF file, output XML, create a ZIP file on the fly, anything
you want, using whatever Python libraries you want.

All Django wants is that HttpResponse. Or an exception.

from mysite.polls.models import Poll
from django.http import HttpResponse

def index(request):
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
output = ', '.join([p.question for p in latest_poll_list])
return HttpResponse(output)

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

def index(request):
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
t = loader.get_template('polls/index.html')
c = Context({
'latest_poll_list': latest_poll_list,
})
return HttpResponse(t.render(c))

Raising 404

Now, let’s tackle the poll detail view — the page that displays the question

for a given poll. Here’s the view:

from django.http import Http404
# ...
def detail(request, poll_id):
try:
p = Poll.objects.get(pk=poll_id)
except Poll.DoesNotExist:
raise Http404
return render_to_response('polls/detail.html', {'poll': p})

A shortcut: get_object_or_404()

It’s a very common idiom to use get() and raise Http404 if the object doesn’t exist. Django provides a shortcut. Here’s the detail() view, rewritten:

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

The get_object_or_404() function takes a Django model module as its first argument and an arbitrary number of keyword arguments, which it passes to the module’s get() function. It raises Http404 if the object doesn’t exist.

There’s also a get_list_or_404() function, which works just as get_object_or_404() — except using filter() instead of get(). It raises Http404 if the list is empty.

Simplifying the URLconfs

Take some time to play around with the views and template system. As you edit the URLconf, you may notice there’s a fair bit of redundancy in it:

urlpatterns = patterns('',
(r'^polls/$', 'mysite.polls.views.index'),
(r'^polls/(?P\d+)/$', 'mysite.polls.views.detail'),
(r'^polls/(?P\d+)/results/$', 'mysite.polls.views.results'),
(r'^polls/(?P\d+)/vote/$', 'mysite.polls.views.vote'),
)

Namely, mysite.polls.views is in every callback.

Because this is a common case, the URLconf framework provides a shortcut for common prefixes. You can factor out the common prefixes and add them as the first argument to patterns(), like so:

urlpatterns = patterns('mysite.polls.views',
(r'^polls/$', 'index'),
(r'^polls/(?P\d+)/$', 'detail'),
(r'^polls/(?P\d+)/results/$', 'results'),
(r'^polls/(?P\d+)/vote/$', 'vote'),
)

This is functionally identical to the previous formatting. It’s just a bit tidier.

Decoupling the URLconfs

While we’re at it, we should take the time to decouple our poll-app URLs from our Django project configuration. Django apps are meant to be pluggable — that is, each particular app should be transferrable to another Django installation with minimal fuss.

Our poll app is pretty decoupled at this point, thanks to the strict directory structure that python manage.py startapp created, but one part of it is coupled to the Django settings: The URLconf.

We’ve been editing the URLs in mysite/urls.py, but the URL design of an app is specific to the app, not to the Django installation — so let’s move the URLs within the app directory.

Copy the file mysite/urls.py to mysite/polls/urls.py. Then, change mysite/urls.py to remove the poll-specific URLs and insert an include():

(r'^polls/', include('mysite.polls.urls')),

include(), simply, references another URLconf. Note that the regular expression doesn’t have a $ (end-of-string match character) but has the trailing slash. Whenever Django encounters include(), it chops off whatever part of the URL matched up to that point and sends the remaining string to the included URLconf for further processing.

Here’s what happens if a user goes to “/polls/34/” in this system:

  • Django will find the match at '^polls/'
  • It will strip off the matching text ("polls/") and send the remaining text — "34/" — to the ‘mysite.polls.urls’ urlconf for further processing.

Now that we’ve decoupled that, we need to decouple the ‘mysite.polls.urls’ urlconf by removing the leading “polls/” from each line:

urlpatterns = patterns('mysite.polls.views',
(r'^$', 'index'),
(r'^(?P\d+)/$', 'detail'),
(r'^(?P\d+)/results/$', 'results'),
(r'^(?P\d+)/vote/$', 'vote'),
)

The idea behind include() and URLconf decoupling is to make it easy to plug-and-play URLs. Now that polls are in their own URLconf, they can be placed under “/polls/”, or under “/fun_polls/”, or under “/content/polls/”, or any other URL root, and the app will still work.

All the poll app cares about is its relative URLs, not its absolute URLs.

No comments: