mirror of
https://github.com/django/django.git
synced 2024-12-25 02:26:12 +00:00
5595fe2aa6
git-svn-id: http://code.djangoproject.com/svn/django/trunk@745 bcc190cf-cafb-0310-a4f2-bffc1f526a37
466 lines
19 KiB
Plaintext
466 lines
19 KiB
Plaintext
=====================================
|
|
Writing your first Django app, part 3
|
|
=====================================
|
|
|
|
By Adrian Holovaty <holovaty@gmail.com>
|
|
|
|
This tutorial begins where `Tutorial 2`_ left off. We're continuing the Web-poll
|
|
application and will focus on creating the public interface -- "views."
|
|
|
|
.. _Tutorial 2: http://www.djangoproject.com/documentation/tutorial2/
|
|
|
|
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
|
|
================
|
|
|
|
The first step of writing views is to design your URL structure. You do this by
|
|
creating a Python module, called a URLconf. URLconfs are how Django associates
|
|
a given URL with given Python code.
|
|
|
|
When a user requests a Django-powered page, the system looks at the
|
|
``ROOT_URLCONF`` setting, which contains a string in Python dotted syntax.
|
|
Django loads that module and looks for a module-level variable called
|
|
``urlpatterns``, which is a sequence of tuples in the following format::
|
|
|
|
(regular expression, Python callback function [, optional dictionary])
|
|
|
|
Django starts at the first regular expression and makes its way down the list,
|
|
comparing the requested URL against each regular expression until it finds one
|
|
that matches.
|
|
|
|
When it finds a match, Django calls the Python callback function, with an
|
|
``HTTPRequest`` object as the first argument, any "captured" values from the
|
|
regular expression as keyword arguments, and, optionally, arbitrary keyword
|
|
arguments from the dictionary (an optional third item in the tuple).
|
|
|
|
For more on ``HTTPRequest`` objects, see the `request and response documentation`_.
|
|
|
|
When you ran ``django-admin.py startproject myproject`` at the beginning of
|
|
Tutorial 1, it created a default URLconf in ``myproject/settings/urls/main.py``.
|
|
It also automatically set your ``ROOT_URLCONF`` setting to point at that file::
|
|
|
|
ROOT_URLCONF = 'myproject.settings.urls.main'
|
|
|
|
Time for an example. Edit ``myproject/settings/urls/main.py`` so it looks like
|
|
this::
|
|
|
|
from django.conf.urls.defaults import *
|
|
|
|
urlpatterns = patterns('',
|
|
(r'^polls/$', 'myproject.apps.polls.views.polls.index'),
|
|
(r'^polls/(?P<poll_id>\d+)/$', 'myproject.apps.polls.views.polls.detail'),
|
|
(r'^polls/(?P<poll_id>\d+)/results/$', 'myproject.apps.polls.views.polls.results'),
|
|
(r'^polls/(?P<poll_id>\d+)/vote/$', 'myproject.apps.polls.views.polls.vote'),
|
|
)
|
|
|
|
This is worth a review. When somebody requests a page from your Web site --
|
|
say, "/polls/23/", Django will load this Python module, because it's pointed to
|
|
by the ``ROOT_URLCONF`` setting. It finds the variable named ``urlpatterns``
|
|
and traverses the regular expressions in order. When it finds a regular
|
|
expression that matches -- ``r'^polls/(?P<poll_id>\d+)/$'`` -- it loads the
|
|
associated Python package/module: ``myproject.polls.views.polls.detail``. That
|
|
corresponds to the function ``detail()`` in ``myproject/polls/views/polls.py``.
|
|
Finally, it calls that ``detail()`` function like so::
|
|
|
|
detail(request=<HttpRequest object>, poll_id=23)
|
|
|
|
The ``poll_id=23`` part comes from ``(?P<poll_id>\d+)``. Using
|
|
``(?P<name>pattern)`` "captures" the text matched by ``pattern`` and sends it
|
|
as a keyword argument to the view function.
|
|
|
|
Because the URL patterns are regular expressions, there really is no limit on
|
|
what you can do with them. And there's no need to add URL cruft such as
|
|
``.php`` -- unless you have a sick sense of humor, in which case you can do
|
|
something like this::
|
|
|
|
(r'^polls/latest\.php$', 'myproject.apps.polls.views.polls.index'),
|
|
|
|
But, don't do that. It's stupid.
|
|
|
|
If you need help with regular expressions, see `Wikipedia's entry`_ and the
|
|
`Python documentation`_. Also, the O'Reilly book "Mastering Regular
|
|
Expressions" by Jeffrey Friedl is fantastic.
|
|
|
|
Finally, a performance note: These regular expressions are compiled the first
|
|
time the URLconf module is loaded. They're super fast.
|
|
|
|
.. _Wikipedia's entry: http://en.wikipedia.org/wiki/Regular_expression
|
|
.. _Python documentation: http://www.python.org/doc/current/lib/module-re.html
|
|
.. _request and response documentation: http://www.djangoproject.com/documentation/request_response/
|
|
|
|
Write your first view
|
|
=====================
|
|
|
|
Well, we haven't created any views yet -- we just have the URLconf. But let's
|
|
make sure Django is following the URLconf properly.
|
|
|
|
Fire up the Django development Web server::
|
|
|
|
django-admin.py runserver --settings="myproject.settings.main"
|
|
|
|
(If you're coming here straight from Tutorial 2, note that we're now running
|
|
the server with ``--settings=myproject.settings.main`` instead of
|
|
``--settings=myproject.settings.admin``. You'll need to restart the server to
|
|
change the ``settings`` parameter.)
|
|
|
|
Now go to "http://localhost:8000/polls/" on your domain in your Web browser.
|
|
You should get a Python traceback with the following error message::
|
|
|
|
ViewDoesNotExist: Could not import myproject.apps.polls.views.polls. Error
|
|
was: No module named polls
|
|
|
|
Try "/polls/23/", "/polls/23/results/" and "/polls/23/vote/". The error
|
|
messages should tell you which view Django tried (and failed to find, because
|
|
you haven't written any views yet).
|
|
|
|
Time to write the first view. Create the file ``myproject/apps/polls/views/polls.py``
|
|
and put the following Python code in it::
|
|
|
|
from django.utils.httpwrappers import HttpResponse
|
|
|
|
def index(request):
|
|
return HttpResponse("Hello, world. You're at the poll index.")
|
|
|
|
This is the simplest view possible. Go to "/polls/" in your browser, and you
|
|
should see your text.
|
|
|
|
Now add the following view. It's slightly different, because it takes an
|
|
argument (which, remember, is passed in from whatever was captured by the
|
|
regular expression in the URLconf)::
|
|
|
|
def detail(request, poll_id):
|
|
return HttpResponse("You're looking at poll %s." % poll_id)
|
|
|
|
Take a look in your browser, at "/polls/34/". It'll display whatever ID you
|
|
provide in the URL.
|
|
|
|
Write views that actually do something
|
|
======================================
|
|
|
|
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.
|
|
|
|
Because it's convenient, let's use Django's own database API, which we covered
|
|
in Tutorial 1. Here's one stab at the ``index()`` view, which displays the
|
|
latest 5 poll questions in the system, separated by commas, according to
|
|
publication date::
|
|
|
|
from django.models.polls import polls
|
|
from django.utils.httpwrappers import HttpResponse
|
|
|
|
def index(request):
|
|
latest_poll_list = polls.get_list(order_by=['-pub_date'], limit=5)
|
|
output = ', '.join([p.question for p in latest_poll_list])
|
|
return HttpResponse(output)
|
|
|
|
There's a problem here, though: The page's design is hard-coded in the view. If
|
|
you want to change the way the page looks, you'll have to edit this Python code.
|
|
So let's use Django's template system to separate the design from Python::
|
|
|
|
from django.core import template_loader
|
|
from django.core.template import Context
|
|
from django.models.polls import polls
|
|
from django.utils.httpwrappers import HttpResponse
|
|
|
|
def index(request):
|
|
latest_poll_list = polls.get_list(order_by=['-pub_date'], limit=5)
|
|
t = template_loader.get_template('polls/index')
|
|
c = Context({
|
|
'latest_poll_list': latest_poll_list,
|
|
})
|
|
return HttpResponse(t.render(c))
|
|
|
|
That code loads the template called "polls/index" and passes it a context. The
|
|
context is a dictionary mapping template variable names to Python objects.
|
|
|
|
Reload the page. Now you'll see an error::
|
|
|
|
TemplateDoesNotExist: Your TEMPLATE_DIRS settings is empty.
|
|
Change it to point to at least one template directory.
|
|
|
|
Ah. There's no template yet. First, create a directory, somewhere on your
|
|
filesystem, whose contents Django can access. (Django runs as whatever user
|
|
your server runs.) Don't put them under your document root, though. You
|
|
probably shouldn't make them public, just for security's sake.
|
|
|
|
Then edit ``TEMPLATE_DIRS`` in your ``main.py`` settings file to tell Django
|
|
where it can find templates -- just as you did in the "Customize the admin look
|
|
and feel" section of Tutorial 2.
|
|
|
|
When you've done that, create a directory ``polls`` in your template directory.
|
|
Within that, create a file called ``index.html``. Django requires that
|
|
templates have ".html" extension. Note that our
|
|
``template_loader.get_template('polls/index')`` code from above maps to
|
|
"[template_directory]/polls/index.html" on the filesystem.
|
|
|
|
Put the following code in that template::
|
|
|
|
{% if latest_poll_list %}
|
|
<ul>
|
|
{% for poll in latest_poll_list %}
|
|
<li>{{ poll.question }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p>No polls are available.</p>
|
|
{% endif %}
|
|
|
|
Load the page in your Web browser, and you should see a bulleted-list
|
|
containing the "What's up" poll from Tutorial 1.
|
|
|
|
A shortcut: render_to_response()
|
|
--------------------------------
|
|
|
|
It's a very common idiom to load a template, fill a context and return an
|
|
``HttpResponse`` object with the result of the rendered template. Django
|
|
provides a shortcut. Here's the full ``index()`` view, rewritten::
|
|
|
|
from django.core.extensions import render_to_response
|
|
from django.models.polls import polls
|
|
|
|
def index(request):
|
|
latest_poll_list = polls.get_list(order_by=['-pub_date'], limit=5)
|
|
return render_to_response('polls/index', {'latest_poll_list': latest_poll_list})
|
|
|
|
Note that we no longer need to import ``template_loader``, ``Context`` or
|
|
``HttpResponse``.
|
|
|
|
The ``render_to_response()`` function takes a template name as its first
|
|
argument and a dictionary as its optional second argument. It returns an
|
|
``HttpResponse`` object of the given template rendered with the given context.
|
|
|
|
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.core.exceptions import Http404
|
|
def detail(request, poll_id):
|
|
try:
|
|
p = polls.get_object(pk=poll_id)
|
|
except polls.PollDoesNotExist:
|
|
raise Http404
|
|
return render_to_response('polls/detail', {'poll': p})
|
|
|
|
The new concept here: The view raises the ``django.core.exceptions.Http404``
|
|
exception if a poll with the requested ID doesn't exist.
|
|
|
|
A shortcut: get_object_or_404()
|
|
-------------------------------
|
|
|
|
It's a very common idiom to use ``get_object()`` and raise ``Http404`` if the
|
|
object doesn't exist. Django provides a shortcut. Here's the ``detail()`` view,
|
|
rewritten::
|
|
|
|
from django.core.extensions import get_object_or_404
|
|
def detail(request, poll_id):
|
|
p = get_object_or_404(polls, pk=poll_id)
|
|
return render_to_response('polls/detail', {'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_object()`` function. It raises ``Http404`` if the object doesn't
|
|
exist.
|
|
|
|
.. admonition:: Philosophy
|
|
|
|
Why do we use a helper function ``get_object_or_404()`` instead of
|
|
automatically catching the ``*DoesNotExist`` exceptions at a higher level,
|
|
or having the model API raise ``Http404`` instead of ``*DoesNotExist``?
|
|
|
|
Because that would couple the model layer to the view layer. One of the
|
|
foremost design goals of Django is to maintain loose coupling.
|
|
|
|
There's also a ``get_list_or_404()`` function, which works just as
|
|
``get_object_or_404()`` -- except using ``get_list()`` instead of
|
|
``get_object()``. It raises ``Http404`` if the list is empty.
|
|
|
|
Write a 404 (page not found) view
|
|
=================================
|
|
|
|
When you raise ``Http404`` from within a view, Django will load a special view
|
|
devoted to handling 404 errors. It finds it by looking for the variable
|
|
``handler404``, which is a string in Python dotted syntax -- the same format
|
|
the normal URLconf callbacks use. A 404 view itself has nothing special: It's
|
|
just a normal view.
|
|
|
|
You normally won't have to bother with writing 404 views. By default, URLconfs
|
|
have the following line up top::
|
|
|
|
from django.conf.urls.defaults import *
|
|
|
|
That takes care of setting ``handler404`` in the current module. As you can see
|
|
in ``django/conf/urls/defaults.py``, ``handler404`` is set to
|
|
``'django.views.defaults.page_not_found'`` by default.
|
|
|
|
Three more things to note about 404 views:
|
|
|
|
* The 404 view is also called if Django doesn't find a match after checking
|
|
every regular expression in the URLconf.
|
|
* If you don't define your own 404 view -- and simply use the default,
|
|
which is recommended -- you still have one obligation: To create a
|
|
``404.html`` template in the root of your template directory. The default
|
|
404 view will use that template for all 404 errors.
|
|
* If ``DEBUG`` is set to ``True`` (in your settings module) then your 404
|
|
view will never be used, and the traceback will be displayed instead.
|
|
|
|
Write a 500 (server error) view
|
|
===============================
|
|
|
|
Similarly, URLconfs may define a ``handler500``, which points to a view to call
|
|
in case of server errors. Server errors happen when you have runtime errors in
|
|
view code.
|
|
|
|
Use the template system
|
|
=======================
|
|
|
|
Back to our ``polls.detail`` view. Given the context variable ``poll``, here's
|
|
what the template might look like::
|
|
|
|
<h1>{{ poll.question }}</h1>
|
|
<ul>
|
|
{% for choice in poll.get_choice_list %}
|
|
<li>{{ choice.choice }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
The template system uses dot-lookup syntax to access variable attributes. In
|
|
the example of ``{{ poll.question }}``, first Django does a dictionary lookup
|
|
on the object ``poll``. Failing that, it tries attribute lookup -- which works,
|
|
in this case. If attribute lookup had failed, it would've tried calling the
|
|
method ``question()`` on the poll object.
|
|
|
|
Method-calling happens in the ``{% for %}`` loop: ``poll.get_choice_list`` is
|
|
interpreted as the Python code ``poll.get_choice_list()``, which returns a list
|
|
of Choice objects and is suitable for iteration via the ``{% for %}`` tag.
|
|
|
|
See the `template guide`_ for full details on how templates work.
|
|
|
|
.. _template guide: http://www.djangoproject.com/documentation/templates/
|
|
|
|
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/$', 'myproject.apps.polls.views.polls.index'),
|
|
(r'^polls/(?P<poll_id>\d+)/$', 'myproject.apps.polls.views.polls.detail'),
|
|
(r'^polls/(?P<poll_id>\d+)/results/$', 'myproject.apps.polls.views.polls.results'),
|
|
(r'^polls/(?P<poll_id>\d+)/vote/$', 'myproject.apps.polls.views.polls.vote'),
|
|
)
|
|
|
|
Namely, ``myproject.apps.polls.views.polls`` 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('myproject.apps.polls.views.polls',
|
|
(r'^polls/$', 'index'),
|
|
(r'^polls/(?P<poll_id>\d+)/$', 'detail'),
|
|
(r'^polls/(?P<poll_id>\d+)/results/$', 'results'),
|
|
(r'^polls/(?P<poll_id>\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 ``django-admin.py startapp`` created, but one part of it is
|
|
coupled to the Django settings: The URLconf.
|
|
|
|
We've been editing the URLs in ``myproject/settings/urls/main.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.
|
|
|
|
Create a directory ``myproject/apps/polls/urls/``, and put a blank file called
|
|
``__init__.py`` into it. (The ``__init__.py`` file is necessary for Python to
|
|
treat the directory as a package.) Then copy the file
|
|
``myproject/settings/urls/main.py`` to ``myproject/apps/polls/urls/polls.py``.
|
|
|
|
Then, change ``myproject/settings/urls/main.py`` to remove the poll-specific
|
|
URLs and insert an ``include()``::
|
|
|
|
(r'^polls/', include('myproject.apps.polls.urls.polls')),
|
|
|
|
``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 'myproject.apps.polls.urls.polls' urlconf for
|
|
further processing.
|
|
|
|
Now that we've decoupled that, we need to decouple the
|
|
'myproject.apps.polls.urls.polls' urlconf by removing the leading "polls/"
|
|
from each line::
|
|
|
|
urlpatterns = patterns('myproject.apps.polls.views.polls',
|
|
(r'^$', 'index'),
|
|
(r'^(?P<poll_id>\d+)/$', 'detail'),
|
|
(r'^(?P<poll_id>\d+)/results/$', 'results'),
|
|
(r'^(?P<poll_id>\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.
|
|
|
|
When you're comfortable with writing views, read `part 4 of this tutorial`_ to
|
|
learn about simple form processing and generic views.
|
|
|
|
.. _part 4 of this tutorial: http://www.djangoproject.com/documentation/tutorial4/
|