Added docs/tutorial03
git-svn-id: http://code.djangoproject.com/svn/django/trunk@195 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
7d553c0650
commit
1f4379bcd4
|
@ -457,15 +457,8 @@ inclusion in the admin index template. It's a useful starting point.
|
|||
For full details on customizing the look and feel of the Django admin site in
|
||||
general, see the `Django admin CSS guide`_.
|
||||
|
||||
When you're comfortable with the admin site, read `part 3 of this tutorial`_ to
|
||||
start working on public poll views.
|
||||
|
||||
.. _Django admin CSS guide: http://www.djangoproject.com/documentation/admin_css/
|
||||
|
||||
Coming soon
|
||||
===========
|
||||
|
||||
The tutorial ends here for the time being. But check back within 48 hours for
|
||||
the next installments:
|
||||
|
||||
* Writing public-facing apps
|
||||
* Using the cache framework
|
||||
* Using the RSS framework
|
||||
* Using the comments framework
|
||||
.. _part 3 of this tutorial: http://www.djangoproject.com/documentation/tutorial3/
|
||||
|
|
|
@ -0,0 +1,414 @@
|
|||
=====================================
|
||||
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 three 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`` request 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).
|
||||
|
||||
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. 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
|
||||
``(?<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
|
||||
|
||||
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.
|
||||
|
||||
Set your ``DJANGO_SETTINGS_MODULE`` environment variable to your main settings
|
||||
(``myproject.settings.main``), as we did with the admin settings in Tutorial 2.
|
||||
Then, fire up the Django development Web server, as we also did in Tutorial 2::
|
||||
|
||||
django-admin.py runserver
|
||||
|
||||
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: myproject.polls.views.polls.index
|
||||
|
||||
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. Restart your development server and go to
|
||||
"/polls/". 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', 'DESC')], 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.extensions import DjangoContext as 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', 'DESC')], limit=5)
|
||||
t = template_loader.get_template('polls/index')
|
||||
c = Context(request, {
|
||||
'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 %}
|
||||
|
||||
Templates are read from disk at each page request, so you don't have to restart
|
||||
the server to see changes. Load the page in your Web browser, and you should
|
||||
see a bulleted-list containing the "What's up" poll from Tutorial 1.
|
||||
|
||||
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(id__exact=poll_id)
|
||||
except polls.PollDoesNotExist:
|
||||
raise Http404
|
||||
t = template_loader.get_template('polls/detail')
|
||||
c = Context(request, {
|
||||
'poll': p,
|
||||
})
|
||||
return HttpResponse(t.render(c))
|
||||
|
||||
The new concept here: The view raises the ``django.core.exceptions.Http404``
|
||||
exception if a poll with the requested ID doesn't exist.
|
||||
|
||||
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.
|
||||
|
||||
Two 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.
|
||||
|
||||
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 ``choice()`` 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.
|
||||
|
||||
Just copy the file ``myproject/settings/urls/main.py`` to
|
||||
``myproject/apps/polls/urls/polls.py``, which had already been created, as a
|
||||
stub, by ``django-admin.py startapp``.
|
||||
|
||||
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')),
|
||||
|
||||
Notes:
|
||||
|
||||
``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.
|
||||
|
||||
Coming soon
|
||||
===========
|
||||
|
||||
The tutorial ends here for the time being. But check back within 48 hours for
|
||||
the next installments:
|
||||
|
||||
* Advanced view features: Form processing
|
||||
* Using the RSS framework
|
||||
* Using the cache framework
|
||||
* Using the comments framework
|
||||
* Advanced admin features: Permissions
|
||||
* Advanced admin features: Custom JavaScript
|
Loading…
Reference in New Issue