===================================== 编写你的第一个 Django 程序, 第3节 ===================================== 这部分教程延续了 :doc:`第二部分 `. 我们将继续完善我们的 Web-poll 程序, 我们将把焦点放在创建公共的接口上 -- 也就是"视图". 哲理 ========== 视图通常是一个在你创建的Django程序中拥有特定功能并拥有一个特定模板的页面. 比如说, 在一个Weblog程序中, 你可能会有一下几个视图: * Bolg 首页 -- 显示最新的几项内容 * 条目的详细页面 -- 一个条目的永久链接页面 * 以年归类的页面 -- 显示给定年份所有月份的内容条目 * 以月归类的页面 -- 显示给定月份所有天的内容条目 * 以天归类的页面 -- 显示给定天的所有内容条目 * 评论功能 -- 处理给某个内容的评价 在我们的投票程序中, 将会实现以下四个视图: * 投票的 "首页" -- 显示最新的几个投票. * 投票的 "详细" 页面 -- 显示一个投票的问题, 只显示投票的表单, 不显示投票的结果. * 投票的 "结果" 页面 -- 显示一个投票的详细结果 * 投票处理 -- 处理一个投票中的一个选项 Django中, 每个视图表现为一个简单的Python函数 设计你的URL ================ 编写视图的地一个步骤是设计你的URL结构. 你需要通过编写一个Python的模块来完成这项工作, 这个模块叫做URLconfs. 这个模块会将Python代码和给定的URL联系起来. 当用户获取一个Django页面, Django会查询 :setting:`ROOT_URLCONF` 配置项. 其中含有 Python代码. Django载入指定模块并定位名为 ``urlpatterns`` 的变量. ``urlpatterns`` 格式如下:: (regular expression, Python callback function [, optional dictionary]) Django会根据请求的URL从列表中的地一个正则表达式开始进行匹配, 直到寻找到相应的匹配. 当寻找到相应的匹配后, Django会调用Python的回调函数, 以 :class:`~django.http.HttpRequest` 对象 作为这个回调函数的第一个参数, 任何从正则表达式 "捕获" 到的值作为关键字参数, ``optional dictionary`` 中的key-value作为可选的关键字参数(元组的第三项). :class:`~django.http.HttpRequest` 类的更多细节, 请参阅 :doc:`/ref/request-response`. URLconfs的更多细节, 请参阅 :doc:`/topics/http/urls`. 当你在第一部分中运行 ``django-admin.py startproject mysite`` 命令的时候, 程序在 ``mysite/urls.py`` 中创建了一个默认的URLconf配置. 并且自动设置了 :setting:`ROOT_URLCONF` 配置 来指向相应的文件:: ROOT_URLCONF = 'mysite.urls' 示例:编辑 ``mysite/urls.py`` 文件, 如下:: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', url(r'^polls/$', 'polls.views.index'), url(r'^polls/(?P\d+)/$', 'polls.views.detail'), url(r'^polls/(?P\d+)/results/$', 'polls.views.results'), url(r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), url(r'^admin/', include(admin.site.urls)), ) 让我们来回顾下. 当某个人访问你网站的某个页面的时候 -- 如"/poll/23/", Django将加载 这个Python的模块, 因为他通过 :setting:`ROOT_URLCONF` 设置中的 ``urlpatterns`` 变量寻找 到了相应的匹配. 当寻找到 ``r'^polls/(?P\d+)/$'`` 这个匹配时, Django就会 加载 ``polls/view.py`` 中的 ``detail()`` 函数. 最后会调用这个函数:: detail(request=, poll_id='23') ``poll_id='23'`` 这部分来自于 ``(?P\d+)``. 使用括号将变量包围, 并将匹配到的内容作为变量传递给视图函数; ``?P`` 定义了用于匹配的名称; ``\d+`` 是一个表示数字的正则表达式. 由于URL的定义是正则表达式, 它和你要做什么没有任何相似点. 而且也不需要写丑陋的如 ``.php`` 这样的扩展名 -- 除非你有种病态的幽默感, 你可以想这样做:: (r'^polls/latest\.php$', 'polls.views.index'), 但是, 千万不要这样做, 这样做非常不和谐. 请注意, 正则表达式不会搜索GET和POST参数, 或者是域名参数. 例如在 ``http://www.ex.com/myapp/`` 这个请求中, URLconf会寻找 ``myapp/`` 这个匹配. 在 ``http://www.example.com/myapp/?page=3`` 这个请求中, URLconf也会来寻找 ``myapp/`` 这个匹配. 如果你在正则表达式上遇到困难, 请参照 `Wikipedia's entry`_ 和 :mod:`re` 模块的文档. O'Reilly出版的由Jeffrey Friedl 编写的 "Mastering Regular Expressions" 这本书是很好的参考书. 最后提下性能上的问题: 这些正则表达式会在第一次执行的时候编译加载, 执行速度非常快. .. _Wikipedia's entry: http://en.wikipedia.org/wiki/Regular_expression 编写你的第一个视图 ===================== 目前为止我们之编写了URLconf配置, 并没有编写任何的视图代码. 但是, 让我们确保我们配置的URLconf可以被Django正确的加载执行. 运行Django的开发服务器: .. code-block:: bash python manage.py runserver 启动后我们访问 "http://localhost:8000/polls/", 你将会看到一个包含以下错误信息的漂亮的错误页面:: ViewDoesNotExist at /polls/ Could not import polls.views.index. View does not exist in module polls.views. 这个错误产生的原因是因为你没有在 ``polls/views.py`` 这个模块中编写 ``index()`` 这个处理函数. 尝试访问 "/polls/23/", "/polls/23/results/" 和 "/polls/23/vote/" 这几个页面. 这个错误信息会直观的告诉你Django可以访问的url包括哪些, 不可访问的有哪些. 现在是编写地一个视图的时候了. 打开 ``polls/view.py`` 文件, 并且将以下代码编写在文件中:: from django.http import HttpResponse def index(request): return HttpResponse("Hello, world. You're at the poll index.") 这可能是一个最简单的视图了. 现在让我们访问下 "/polls/" 这个url, 你会看到写在文件中的字符串. 现在让我们多编写几个视图. 这些视图有一些小小的不同, 因为他们会带有一个参数 (请记访问地址会被任何匹配的正则表达式所拦截):: def detail(request, poll_id): return HttpResponse("You're looking at poll %s." % poll_id) def results(request, poll_id): return HttpResponse("You're looking at the results of poll %s." % poll_id) def vote(request, poll_id): return HttpResponse("You're voting on poll %s." % poll_id) 现在再让我们来查看下 "/polls/34" 这个地址. 这个地址的访问会运行 `detail()` 这个方法并且会显示你在URL中传递的的参数的值. 然后在尝试访问 "/polls/34/results/" 和 "/polls/34/vote/" 这两个地址会分别显示投票的结果页面和投票页面. 编写一个真正有实际作用的视图 ====================================== 每个视图的相应都会做相应的一件事或者更多的事情: 返回一个包含含有请求页面内容的 :class:`~django.http.HttpResponse` 对象, 或者抛出一个类似于 :exc:`~django.http.Http404` 这样的错误, 这完全取决与你自己的处理方式. 你的视图可以访问数据库中的数据, 也可以不访问. 他可以使用Django自己的模板系统或者第三方的模板系统来渲染页面. 也可以创建PDF文件, XML文件或者一个ZIP文件, 使用你想使用的python库来做任何你想做的事情. Django只关心你返回的 :class:`~django.http.HttpResponse` 对象, 或者是返回的异常. 因为Django自己的数据库API使用非常方便, 这里就使用 :doc:`Tutorial 1 ` 中介绍过的方法. 下面这个方法获取了五条最新的投票内容, 并用逗号进行分隔:: from 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) 这里会产生一个问题, 试想一下, 在视图中设计页面代码是十分困难的. 如果你想改变页面的整体设计, 你必须要编辑视图中的Python代码. 所以让我们使用Django中提供的模板系统来解决页面的设计问题, 将python代码和前台页面分离开来:: from django.template import Context, loader from 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)) 这部分代码加载了叫做 "polls/index.html" 的模板, 并且将一部分内容传递给了模板, 这部分内容是一个字典组成的变量, 用于将Python的对象和模板的变量进行对应. 重新加载这个页面, 现在你将会看到如下的错误:: TemplateDoesNotExist at /polls/ polls/index.html 目前我们还没有创建模板. 首先需要在你的文件系统上创建一个Django系统可以访问的目录, 不要把他们放在你的文档目录下面. 为了安全着想, 你可能不会把他们设置成任何人都可以浏览的. 然后编辑你工程中的 ``settings.py`` 文件中的 :setting:`TEMPLATE_DIRS` 选项来告诉Django应该去哪里寻找你编辑的模板, 就像你在本教程第二部分做的那样. 当你完成以上的工作后, 在你的模板目录中创建一个 ``poll`` 目录, 并创建一个叫做 ``index.html`` 的文件. 请确认 ``loader.get_template('polls/index.html')`` 代码对应的 "[template_directory]/polls/index.html" 文件在当前的文件系统中可以找到. 把下面这部分代码放在你的模板文件中: .. code-block:: html+django {% if latest_poll_list %} {% else %}

No polls are available.

{% endif %} 在浏览器中访问这个页面你会得到一个包含有 "What's up" 内容的投票条目, 这个链接会链接到投票的详细页面. 捷径: render_to_response() -------------------------------- 这是个常用的加载模板, 填充内容并且返回一个带有模板渲染内容的 :class:`~django.http.HttpResponse` 对象的Django方法. 这里是个完整的改写后的 ``index()`` 视图:: from django.shortcuts import render_to_response from polls.models import Poll def index(request): latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list}) 一旦我们在所有的视图中完成上面的工作, 我们就不需要再引入如下内容 :mod:`~django.template.loader`, :class:`~django.template.Context` 和 :class:`~django.http.HttpResponse`. :func:`~django.shortcuts.render_to_response` 函数需要一个模板的名称作为地一个输入参数, 第二个参数为可选的字典对象. 这个函数会返回一个根据指定内容渲染后的包含 :class:`~django.http.HttpResponse` 的模板对象. 抛出404错误 =========== 现在让我们处理投票的详细页面 -- 显示给定投票的所有问题选项, 下面是他的视图:: 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不存在的情况下返回一个 :exc:`~django.http.Http404` 错误. 我们晚点再来讨论在 ``polls/detail.html`` 中需要放哪些内容, 如果你想尽快的使模板工作起来, 就在模板中输入以下内容好了:: {{ poll }} 现在就可以使用了. 介绍: get_object_or_404() ------------------------------- 这是个在你使用 :meth:`~django.db.models.query.QuerySet.get` 对象查询的结果不存在的情况下并且抛出 :exc:`~django.http.Http404` 异常时常用的语法. Django提供了这个简单的方式来进行处理. 下面是修改后的 ``detail()`` 视图:: 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}) :func:`~django.shortcuts.get_object_or_404` 函数使用一个Django的数据模型作为第一个输入参数, 传递到 :meth:`~django.db.models.query.QuerySet.get` 函数中任意一个数字作为第二个关键字参数. 当查询的对象不存在的时候会产生 :exc:`~django.http.Http404` 错误. .. admonition:: 哲学 为什么我们会使用 :func:`~django.shortcuts.get_object_or_404` 这个方法来自动捕获 :exc:`~django.core.exceptions.ObjectDoesNotExist` 异常, 或者使用模型API来产生 :exc:`~django.http.Http404` 错误来取代 :exc:`~django.core.exceptions.ObjectDoesNotExist`? 因为这样做会将模型层和视图层联系起来, Django设计的目的很重要的一个理念就是松耦合. 这里还有个类似的 :func:`~django.shortcuts.get_list_or_404` 函数, 工作方式和 :func:`~django.shortcuts.get_object_or_404` 类似 -- 使用 :meth:`~django.db.models.query.QuerySet.filter` 来取代 :meth:`~django.db.models.query.QuerySet.get`. 查询对象不存在的情况下会产生 :exc:`~django.http.Http404` 异常. 编写一个404错误页面 ================================= 当你在视图中抛出一个 :exc:`~django.http.Http404` 异常的时候, Django会加载一个特殊的视图来处理相应的404错误. Django会在你的URLconf (仅仅是这个文件中其他任何地方的配置均无效)中来寻找 ``handler404`` 变量, 是由 python 语法来进行编写的 -- 格式同普通的url配置相同. 404视图和普通视图没有任何差别. 通常情况下你不需要为编写404视图而困惑. 如果你不设置 ``handler404`` 变量, Django 内置的 :func:`django.views.defaults.page_not_found` 会被在默认情况下调用. 这个例子中你需要在你模板根目录下创建一个 ``404.html`` 页面. 默认的404视图将会使用这个页面来处理所有的404错误. 如果 :setting:`DEBUG` 被设置为 ``False`` (在设置文件内)并且你没有创建相应的 ``404.html`` 文件, 服务器就会产生 ``Http500`` 错误, 所以请记得创建 ``404.html`` 文件. 一些404视图需要注意的地方: * 如果 :setting:`DEBUG` 的值被设置成了 ``True`` , 你自定义的404页面永远都不会被执行, 因为Django自己的调试页面会取代这个页面. * 404错误视图也会在Django没有通过正则表达式找到任何匹配的url时产生. 编写一个500视图 =============================== 与404错误类似, 你需要在URLconf中定义一个 ``handler500`` 变量, 来将服务器的错误和你自定义的视图联系起来. 当程序运行出现错误的时候服务器会产生这个错误. 使用模板系统 ======================= 让我们回到投票程序的 ``detail()`` 视图中来. 根据定义的``poll``变量, 我们设计的 "polls/detail.html" 模板内容如下所示: .. code-block:: html+django

{{ poll.question }}

    {% for choice in poll.choice_set.all %}
  • {{ choice.choice }}
  • {% endfor %}
模板系统会使用圆点语法来访问变量的属性. ``{{ poll.question }}`` 这个例子中, Django 首先对 ``poll`` 对象进行字典搜索. 如果没找到相应的结果, 就会尝试对象的属性查找. 如果属性查找仍然失败, 就会尝试进行列表查找. :ttag:`{% for %}` 循环中代码执行的过程: ``poll.choice_set.all`` 会被解释成为python中的 ``poll.choice_set.all()`` 代码, 这部分代码会返交给 :ttag:`{% for %}` 标签使用的包含查询对象的字典对象. 请查看 :doc:`模板概述 ` 来了解更多使用模板的方法. 简化URLconfs的配置 ======================== 多次使用模板系统后, 在我们的 URLconf 文件中你可能注意到了配置文件中的一点冗余:: urlpatterns = patterns('', url(r'^polls/$', 'polls.views.index'), url(r'^polls/(?P\d+)/$', 'polls.views.detail'), url(r'^polls/(?P\d+)/results/$', 'polls.views.results'), url(r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), ) 我们每次都要输入 ``polls.views`` 这部分代码. 因为这是一个经常出现的状况, 所以URLconf框架提供了一个简单的配置方式. 你可以将重复的代码放在 :func:`~django.conf.urls.patterns` 中作为地一个输入参数, 例如:: urlpatterns = patterns('polls.views', url(r'^polls/$', 'index'), url(r'^polls/(?P\d+)/$', 'detail'), url(r'^polls/(?P\d+)/results/$', 'results'), url(r'^polls/(?P\d+)/vote/$', 'vote'), ) 这样做所实现的功能与之前完全相同, 只是看起来稍微整洁一点点. 通常情况下不需要在每个回调函数中加上前缀, 你可以将多个 :func:`~django.conf.urls.patterns` 连接起来. 你完整的 ``mysite.urls.py`` 文件有可能像下面这样:: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('polls.views', url(r'^polls/$', 'index'), url(r'^polls/(?P\d+)/$', 'detail'), url(r'^polls/(?P\d+)/results/$', 'results'), url(r'^polls/(?P\d+)/vote/$', 'vote'), ) urlpatterns += patterns('', url(r'^admin/', include(admin.site.urls)), ) URLconfs中的去耦合 ======================= 当真正开始工作的时候, 我们会花时间将我们投票程序URL和项目配置进行解耦. Django 工程下面每个应用都应该是可插拔的 -- 也就是说, 每个应用都可以在最小的改动下移植到另一个Django的工程中去. 多亏 ``python manage.py startapp`` 创建的严格的目录结构, 我们的投票程序解耦做的 非常好, 但是有一个部分和Django的联系还比较紧密, 那就是URLconf配置. 我们已经在 ``mysite/url.py`` 中配置了相应的URL, 但是这部分URL配置只是单独给这个应用编写的, 而不是给整个Django的项目编写的 -- 所以让我们吧这部分配置移动到应用自己的目录下面去. 将 ``mysite/urls.py`` 文件拷贝到 ``polls/urls.py``. 然后去掉 ``mysite/urls.py`` 中专门为投票应所编写的URL, 并插入一个 :func:`~django.conf.urls.include` 函数:: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', url(r'^polls/', include('polls.urls')), url(r'^admin/', include(admin.site.urls)), ) :func:`~django.conf.urls.include` 简单的引用了另一个URLconf配置. 请注意, 这里的正则表达式没有 ``$`` 符号, 但是有一个反斜线. 无论什么时候Django遇到 :func:`~django.conf.urls.include`, 处理时都会将匹配到的结果的后半部分字符串发送给被包含的那部分URLconf配置中去, 来进行进一步的处理. 下面是说明当你访问 "/polls/34/" 这个地址时系统的处理方式: * 首先Django会寻找到相应的匹配也就是 ``'^polls/'`` * 然后, Django会截断匹配到的字符串, 并将截断后的后半部分也就是 ``"34/"``, 发送给 'polls.urls' 这个URLconf配置文件进行进一步的处理. 现在我们已经将这部分进行了解耦, 我们现在需要修改 ``polls.urls``, 去掉每行代码中前面的 "polls/", 并且去掉在Django管理站点中的注册. 你最后配置的 ``polls/urls.py`` 文件看起来会像是下面的这个样则:: from django.conf.urls import patterns, include, url urlpatterns = patterns('polls.views', url(r'^$', 'index'), url(r'^(?P\d+)/$', 'detail'), url(r'^(?P\d+)/results/$', 'results'), url(r'^(?P\d+)/vote/$', 'vote'), ) :func:`~django.conf.urls.include` 方法和URLconf的解耦合是想创建一个易插拔的URL配置. 现在我们床及爱你的投票应用完全使用他自己的URL配置文件了, 现在这个应用可以放在 "/polls/", 或者 "/fun_polls/", 或者 "/content/polls/" 目录下面, 或者任何其他目录, 而且这个应用仍然会正常的运行. 所有的投票应用之关心相对的路径, 与绝对路径无关. 如果你想更好的处理你的视图, 请阅读 :doc:`第四部分文档 ` 来了解如何处理和生成试图的例子.