Django中的试图概念:一类具有相同功能和模板的网页集合

例如,在一个博客应用中,可能会创建如下视图

  • 博客首页——展示最近的几项内容。
  • 内容“详情”页——详细展示某项内容。
  • 以年为单位的归档页——展示选中的年份里各个月份创建的内容。
  • 以月为单位的归档页——展示选中的月份里各天创建的内容。
  • 以天为单位的归档页——展示选中天里创建的所有内容。
  • 评论处理器——用于响应为一项内容添加评论的操作。

而在我们的投票应用中,我们需要下列几个视图:

  • 问题索引页——展示最近的几个投票问题。
  • 问题详情页——展示某个投票的问题和不带结果的选项列表。
  • 问题结果页——展示某个投票的结果。
  • 投票处理器——用于响应用户为某个问题的特定选项投票的操作。

在Django中,网页和其他内容都是从视图派生而来的,每个试图表现为一个python函数。Django会根据用户请求的URL来选择使用哪一个视图。

URL像是是URL的一般形式:例如:/newsarchive/\/\/…

为了将URL与视图联系起来,DJango使用URLconfs来配置。URLconf将URL模式映射到视图

编写更多视图

我们通过polls/views.py添加更多视图,这些视图有一些不同,因为他们接收参数:

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

把这些新视图添加进polls/urls模块中,只需要添加url()函数调用即可:

from django.urls import path
from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

之后我们访问以下/polls/53/,Django会运行detail()方法并且展示URL里面提供的问题ID,同样可以尝试/polls/53/vote/

image-20220308150513802

image-20220308150537747

当请求网站的某一页面时,例如polls/53,Django会先载入mysite.ruls模块,因它在为配置项ROOT_RULCONF设置了。然后Django寻找名为urlpatterns变量并且按序匹配正则表达式。在找到匹配项polls/后,它会切掉匹配的文本'polls/',将文本'34/',发送至'polls.urls'URLconf会做进一步处理。在这里剩余文本匹配了'\/',使得我们Django以如下形式调用detail():

detail(request=<HttpRequest object>, question_id=53)

问题 question_id=34 来自 <int:question_id>。使用尖括号 "获得" 网址部分后发送给视图函数作为一个关键字参数。字符串的 question_id 部分定义了要使用的名字,用来识别相匹配的模式,而 int 部分是一种转换形式,用来确定应该匹配网址路径的什么模式。冒号 (:) 用来分隔转换形式和模式名。

编写一个真正有用的视图

每个视图必须要做的只有两件事:返回一个包含被请求页面内容的 HttpResponse 对象,或者抛出一个异常,比如 Http404

视图可以从数据库里读取记录,可以使用一个模板引擎(比如Django自带的,或者其他第三方的),可以生成PDF文件、XML,或者创建一个ZIP文件,使用任何想使用的python库,做任何想做的事情

因为Django只要求放回一个HttpResponse,或是抛出一个异常。

接下来我们在index()函数中插入一些新内容,它能展示数据库里以发布日期排序的最近5个投票问题,以空格分隔:

from django.http import HttpResponse
from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

但是这里有一个问题:页面的设计写死在视图函数的代码里了。如果想要改变页面样式,需要编辑python代码。所以我们需要使用Django的模板系统,只需要创建一个视图,就可以将页面的设计从代码中分离出来。

首先在polls目录中创建一个templates目录。Django将会在这个目录里查找模板文件。

项目的TEMPLATES配置项描述了Django如何载入和渲染模板。默认的设置文件设置了DjangoTemplates后端,并将APP_DIRS设置成了True。这一选项将会让DjangoTemplates在每一个INSTALLED_APPS文件中寻找'templates'子目录。

在你刚刚创建的 templates 目录里,再创建一个目录 polls,然后在其中新建一个文件 index.html 。即polls/templates/polls/index.html

虽然我们现在可以将模板文件直接放在 polls/templates 文件夹中(而不是再建立一个 polls 子文件夹),但是这样做不太好。Django 将会选择第一个匹配的模板文件,如果你有一个模板文件正好和另一个应用中的某个模板文件重名,Django 没有办法 区分 它们。我们需要帮助 Django 选择正确的模板,最好的方法就是把他们放入各自的 命名空间 中,也就是把这些模板放入一个和 自身 应用重名的子文件夹里。

接下来将html代码写入到模板中

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>index.html</title>
</head>
<body>
    {% if latest_question_list %}
        <ul>
        {% for question in latest_question_list %}
            <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
        {% endfor %}
        </ul>
    {% else %}
        <p>No polls are available.</p>
    {% endif %}
</body>
</html>

然后更新polls/views.py里的index视图来使用模板:

from django.http import HttpResponse
from django.template import loader

from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))

上述代码的作用是,载入 polls/index.html 模板文件,并且向它传递一个上下文(context)。这个上下文是一个字典,它将模板内的变量映射为 Python 对象。

现在用浏览器访问 "/polls/" ,会看见一个无序列表

image-20220308150426666

快捷函数 render()

「载入模板,填充上下文,再返回由它生成的 HttpResponse对象」是一个非常常用的操作流程。于是 Django 提供了一个快捷函数,我们用它来重写 index() 视图:

from django.shortcuts import render
from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

注意到,我们不再需要导入 loaderHttpResponse。不过如果你还有其他函数(比如说 detail, results, 和 vote )需要用到它的话,就需要保持 HttpResponse 的导入。

抛出 404

如果用户指定的问题ID所对应的问题不存在,我们应当抛出一个404异常。

我们可以在polls/views.py中编辑这个视图代码,将404异常加入进去。

from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

稍后会讨论在polls/detail.html里输入什么,如果想尝试上面的代码是否正常运行的话,可以暂时输入以下代码:

{{ question }}

快捷函数 get_object_or_404()

在使用get()函数获取一个对象时,如果不存在就会抛出一个404异常。Django也提供了一个快捷函数,下面是修改后的detail()视图代码:

from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    context = {
        'question':question,
    }
    return render(request, 'polls/detail.html', context)

同样,也存在get_list_or_404函数,工作原理与get_object_or_404相同,除了get()替换成了filter()

使用模板系统

detail()视图中,它向模板传递了上下文变量question。

下面是polls/detail.html模板中的代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ question.question_text }}</title>
</head>
<body>
    <h1>{{ question.question_text }}</h1>
        <ul>
    {% for choice in question.choice_set.all %}
        <li>{{ choice.choice_text }}</li>
    {% endfor %}
</ul>
</body>
</html>

image-20220308150414238

在模板系统中,使用标点符号来访问变量的属性。在实力{{ question.question_text }}中,Django会对question对象使用字典查找(obj.get(str)),如果操作失败,就会尝试属性查找(obj.str),如果这一操作也失败的话,会尝试列表查找(obj[int])。

在{% for %}循环中发生的函数调用:question.choice_set.all被解释为python代码question.choice_set.all(),会返回一个可迭代的choice对象,这个对象可以在{% for %}标签内部使用。

去除模板中的硬编码URL

在我们polls/index.html里编写的投票链接是硬编码:

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

硬编码会产生一个问题,对于一个包含很多应用的项目而言,修改会很不方便。然而因为在polls.url的url()函数中通过name参数为URL定义了名字,所以可以使用{% url %}标签来代替它:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

这个标签的工作方式是在 polls.urls 模块的 URL 定义中寻具有指定名字的条目。你可以回忆一下,具有名字 'detail' 的 URL 是在如下语句中定义的:

# name 的值由{% url %}模板标签调用
path('<int:question_id>/', views.detail, name='detail'),

如果你想改变投票详情视图的 URL,比如想改成 polls/specifics/12/ ,你不用在模板里修改任何东西(包括其它模板),只要在 polls/urls.py 里稍微修改一下就行:

# 添加specifics
path('specifics/<int:question_id>/', views.detail, name='detail'),

为URL名称添加命名空间

在Django项目,会存在很多应用。例如polls应用有detail视图,另一个博客应用也有相同的视图。此时,我们需要在根URLconf中添加命名空间。在polls/urls.py中稍作修改,加上app_name设置命名空间

from django.urls import path
from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

接着编辑polls/index.html文件:

<!-- 
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> 
-->
<!-- 修改为具有命名空间的视图 -->
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>