Writing your first Django app, part 3

上接 Tutorial 2 . 我们接着开发 Web-poll 应用程序,这次我们关注的是创建界面 -- "views."

哲学

一个 view 就是你的 Django 应用程序中服务于某一功能的使用一个特定模板的同一 "类型" 的网页. 举例来说, 一个 blog 系统, 应该有以下 views:

  • Blog homepage -- 显示最近发布记录.
  • Entry "detail" page -- 某一记录的链接页.
  • Year-based archive page -- 显示给定年内所有月的记录存档.
  • Month-based archive page -- 显示给定月内所有天的记录存档.
  • Day-based archive page -- 显示给定日的所有记录.
  • Comment action -- 处理评论的提交等等.

在我们的 poll 应用程序中, 我们要拥有以下四个 view:

  • Poll "archive" page -- 最近的民意测验.
  • Poll "detail" page -- 显示一个问题, 没有结论但有一个表单可以投票.
  • Poll "results" page -- 显示一个特定测验的投票结果.
  • Vote action -- 处理投票(某一测验的某一选择的投票)

在 Django 中, 每个 view 都由一个简单的 Python 函数来表示.

设计你的 URL

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.

当用户请求一个页面时, Django 首先查看 ROOT_URLCONF 设置, which contains a string in Python dotted syntax. Django 载入这个模块并查找一个模块级变量 urlpatterns, 这是一个以下格式的 tuple 的序列:

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

当发现一个匹配时, Django 调用该 Python 回调函数, 以一个 HTTPRequest 对象作为该函数的第一个参数, 任何从正则表达式中 "captured" 的值作为关键字参数, 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. For more details on URLconfs, see the URLconf documentation.

当你在教程第一部分运行 python manage.py startproject mysite 时,它在 mysite/urls.py 中创建了一个默认的 URLconf .它也自动的设置了你的 ROOT_URLCONF 设置指向该文件 file:

ROOT_URLCONF = 'mysite.urls'

编辑 mysite/urls.py 让它拥有下列内容:

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^polls/$', 'mysite.polls.views.index'),
    (r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
    (r'^polls/(?P<poll_id>\d+)/results/$', 'mysite.polls.views.results'),
    (r'^polls/(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.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: mysite.polls.views.detail. That corresponds to the function detail() in mysite/polls/views.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 parenthesis around a pattern "captures" the text matched by that pattern and sends it as an argument to the view function; the ?P<poll_id> defines the name that will be used to identify the matched pattern; and \d+ is a regular experession to match a sequence of digits (i.e., a number).

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$', 'mysite.polls.views.index'),

But, don't do that. It's silly.

Note that these regular expressions do not search GET and POST parameters, or the domain name. For example, in a request to http://www.example.com/myapp/, the URLconf will look for /myapp/. In a request to http://www.example.com/myapp/?page=3, the URLconf will look for /myapp/.

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.

来写你的第一个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:

python manage.py runserver

Now go to "http://localhost:8000/polls/" on your domain in your Web browser. You should get a pleasantly-colored error page with the following message:

ViewDoesNotExist at /polls/

Tried index in module mysite.polls.views. Error was: 'module'
object has no attribute 'index'

This error happened because you haven't written a function index() in the module mysite/polls/views.py.

Try "/polls/23/", "/polls/23/results/" and "/polls/23/vote/". The error messages tell you which view Django tried (and failed to find, because you haven't written any views yet).

Time to write the first view. Open the file mysite/polls/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.")

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.

写点有用的 view

每个 view 都用来做一件或两件事: 返回一个 HttpResponse 对象(包含请求页面的内容), 或引发一个异常比如 Http404. 剩下的事就该你来了. 你的 view 可以从数据库中读取记录,也可以不用数据库.它可以使用模板--Django 的或第三方的模板系统, 也可以不使用模板.它可以生成 pdf 文件, 可以输出 xml , 可以即时生成 zip 文件. 它可以做你想做的任何事, 使用你想用的任何 python 库.

Django 只要求它是一个 HttpResponse 或者是一个异常.

方便起见, 我们使用 Django 自己的数据库 API, (在教程第一部分中我们已经见识过). 这是一个即兴版本的 index() view, 它显示最新的 5 个民意测验问题,根据发布日期排序,用逗号分隔.:

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

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

有一个问题就是: 页面的设计在 view 中是硬编码的. 如果你打算改变页面的样子, 就必须编辑 python 源代码. 所以我们用 Django 的模板系统重写这个 view(实现代码与设计分离):

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')
    t = loader.get_template('polls/index.html')
    c = Context({
        'latest_poll_list': latest_poll_list,
    })
    return HttpResponse(t.render(c))

以上代码载入名为"polls/index.html" 的模板, 并传递给它一个上下文. 上下文是一个映射模板变量名与 Python 对象的字典.

重新载入本面,你会看到下列错误:

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 settings.py 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. Note that our loader.get_template('polls/index.html') 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()

载入一个模板, 根据上下文对其渲染,然后返回一个 HttpResponse 对象. 这几乎是 WEB 开发每时每刻都要面对的事情. 象上面那样写 view 是很累人的. Django 也这么想. 所以 Django 还为懒惰的程序员提供了一个捷径. 下面是完整的 index() view, 使用捷径重写的版本:

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

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

瞧, 我们不用再导入 loader, ContextHttpResponse 了.

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

现在我们来搞定 poll detail 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})

一个新概念: 如果指定的 id 的 poll 不存在, view 会抛出 django.http.Http404 异常.

A shortcut: get_object_or_404()

使用 get_object() 得到对象及在得不到对象时抛出 Http404 是司空见惯的行为. Django 提供了一个捷径来处理这个情况.下面是使用捷径重写的 detail() view 版本:

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_object() function. It raises Http404 if the object doesn't exist.

Philosophy

为什么要使用函数 get_object_or_404() 而不是自动捕获 DoesNotExist 异常, 或者干脆由 model API 抛出 Http404 异常(不抛出``DoesNotExist`` 异常)呢?

因为那样做会在 model 层和 view 层产生耦合. Django 设计的原则之一就是要容易维护, 尽可能减少耦合强度.

同样还有一个 get_list_or_404() 函数, 它与 get_object_or_404() 非常相似 -- 它使用 get_list() 而不是 get_object(). 它在 list 为空时抛出 Http404 异常.

写一个 404 (未找到页面) view

当你在一个 view 中抛出 Http404 异常时, Django 会载入一个特殊的 view 专门处理 404 错误. django 通过查找变量 handler404 来定位这个 view, 该变量是一个 Python 字符串(.语法字符串) -- 同正常 URLconf 回调函数使用的格式一样. 404 view 本身没有特殊的: 它就是一个普通的 view.

通常你根本不需要写一个 404 view. URLconfs 的第一行是:

from django.conf.urls.defaults import *

这就在当前模块中引入了默认的 handler404 变量. 在 django/conf/urls/defaults.py 中你会发现 handler404 被默认设置为 'django.views.defaults.page_not_found' .

关于 404 views 有以下注意事项:

  • 在 Django 在 URLconf 中找不到任何匹配时也会调用 404 view.
  • 如果你没有自定义 404 view -- 仅使用默认值的话--还是建议你在模板根目录下创建一个 404.html 模板.默认情况下 404 view 会对所有的 404 错误应用这个模板.
  • 若在你的 settings 模块中 DEBUG 的值为 True, 则会显示 traceback 信息而不会使用你的自定义 404 view.

写一个 500 (服务器错误) view

类似的, URLconfs 可以定义一个 handler500, 指向一个 view 以处理服务器错误的情况. 当你的 view 中存在运行时会抛出服务器错误异常.

使用模板系统

现在回到 polls.detail view 并提供上下文变量 poll, detail.html 的内容如下:

<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }}</li>
{% endfor %}
</ul>

模板系统使用 dot-lookup 语法访问变量属性. 以 {{ poll.question }} 为例, 首先 Django 对 poll 对象进行字典查询.或查询失败, 再尝试属性查询 -- 对,这就是工作原理. 如果属性查询再次失败, 它就尝试调用 poll 对象的 question() 方法.

{% for %} 循环会触发方法调用: poll.choice_set.all 被解释为 Python 代码 poll.choice_set.all(), 它会返回一个提供 Choice 对象的迭代器用于 {% for %} tag.

参阅 template guide 了解模板工作的完整细节.

精简 URLconfs

Take some time to play around with the views and template system. 在你编辑 URLconf 时, 你会注意到其中有些冗余信息:

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

每个回调函数都包括 mysite.polls.views 部分.

由于这也是相当常见的情况, URLconf 框架提供了一个回调函数前缀功能来减少数据冗余. 将共同部分提取出来将其作为 patterns() 的第一个参数就可以了, 象下面这样:

urlpatterns = patterns('mysite.polls.views',
    (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'),
)

功能与前面提到的完全相同,只是看上去舒服多了.

URLconfs 退耦

忙活到现在, 我们应该花点时间为我们的 poll-app 的 URLs 从 Django project 配置中解除耦合了. Django apps 是可重用的 -- 也就是说, 每个 app 都可以轻松的移植到另一个 Django 安装中.

我们的 poll app 此刻很容易退耦, 这得感谢由 python manage.py startapp 创建的严谨目录结构, 不过有一部分是与 Django settings 耦合的,那就是: The URLconf.

我们刚才一直在 mysite/urls.py 中而不是 mysite/polls/urls.py 中编辑 URLs. 这是一个问题, 一个 app 的 URL 设计应该仅用于该 app, 而不是整个 Django 安装 -- 现在是将 URLs 移动 polls 目录中的时候了.

复制文件 mysite/urls.pymysite/polls/urls.py. 然后编辑 mysite/urls.py 删除所有 poll-专用的 URLs 并插入一行 include():

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

include(), 简单的引用另一个 URLconf. 注意正则表达式并未以 $ (字符串结束标志) 而是以一个反斜线结尾. 当 Django 碰到 include() 时, 就会砍掉 URL中匹配的部分并发送剩下的字符串给被包括的 URLconf 以进行下一步处理.

当请求 "/polls/34/" 页面时,系统这样反应:

现在我们修改 'mysite.polls.urls' urlconf 在每一行中移去 "polls/"

urlpatterns = patterns('mysite.polls.views',
    (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. 现在 polls 拥有自己的 URLconf, 它们可以被放到 "/polls/" 下 ,或者 "/fun_polls/", 或者 "/content/polls/", 或者任意其它的 url root下,这个 app 照样可以工作.

所有的 poll app 只关心自己相关的 URLs, 而不是绝对 URLs.

当你已经可以熟练书写 views 时, 阅读 part 4 of this tutorial 来学习简单表单处理及 generic views.