首页 > 编程笔记

Django入门教程(简明版)

Django 是基于 Python 的开源 Web 框架。

Django 拥有高度定制的对象关系映射(Object Relational mapping,ORM)、大量的 API、简单灵活的视图编写功能、优雅的 URL、适于快速开发的模板以及强大的管理后台等。这些使得它在 Python Web 开发领域占据了不可动摇的地位。

Instagram、 Firefox、国家地理杂志等著名网站都在使用 Django 进行网站开发。

Instagram 图标
图1:Instagram 图标

Django 是基于 MVT(Model-View-Template,模型-视图-模板)模型设计的。

Django 的 MVT 模型
图2:Django 的 MVT 模型

MVT 模型的说明如下:

Django 安装

Django 的安装命令如下所示:

pip install django==2.2

这里安装的是 Django 2.2,它支持 Python 3.5 以及后续版本。当然也可以根据需要安装其他版本,如图3所示。

Django 与 Python 的版本选择
图3:Django 与 Python 的版本选择

若要验证 Django 是否能被 Python 识别,可以在 Shell 中输入 python 并按 Enter 键。然后在 Python 提示符下,尝试导入 Django。
>>> import django
>>> print(django.get_version())
2.2

Django 的请求和响应

接下来通过一个基本的投票应用程序来讲解 Django 的使用。

该应用程序由以下两部分组成:

1) 创建项目

如果是第一次使用 Django,那么需要进行一些初始化设置。也就是说,需要用一些自动生成的代码配置一个 Djangoproject,即一个 Django 项目实例需要的设置项集合,包括数据库配置、Django 配置和应用程序配置。

打开命令行界面,利用 cd 命令切换到一个你想放置代码的目录,然后运行以下命令。

django-admin startproject mysite

这行代码将会实现在当前目录下创建一个 mysite 目录。如果命令运行失败,查看运行 django-admin 时遇到的问题的相关内容,可能能给用户提供帮助。

看一看 startproject 创建了些什么。
mysite/
    manage.py
    mysite/
        _ _init_ _.py
        settings.py
        urls.py
        wsgi.py
这些目录和文件的用处如下:

2) 用于开发的简易服务器

让我们来确认一下 Django 项目是否真的创建成功了。如果当前目录不是外层的 mysite 目录,请切换到此目录,然后运行下面的命令。

python manage.py runserver

会看到如下输出:

Watching for file changes with StatReloader
Performing system checks...
 
System check identified no issues (0 silenced).
 
You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
October 08, 2020 - 16:13:53
Django version 2.2, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

刚刚启动的是 Django 自带的用于开发的简易服务器,它是一个用纯 Python 语言写的轻量级的 Web 服务器。将这个服务器内置在 Django 中是为了让用户能快速开发出想要的东西,因为用户在这阶段不需要开展配置生产级别的服务器(比如 Apache)方面的工作。

现在,服务器正在运行,浏览器访问 http://127.0.0.1:8000/,将会看到一个“祝贺(Congratulations)”页面,页面中有着火箭发射的图标,代表服务器成功运行,如图4所示。

Django 服务器成功运行
图4:Django 服务器成功运行

默认情况下,runserver 命令会将服务器设置为监听本机内部 IP 地址的 8000 端口。

如果想更换服务器的监听端口,请使用命令行参数。举个例子,下面的命令会使服务器监听 8080 端口。

python manage.py runserver 8080

如果想要修改服务器监听的 IP 地址,那么要在端口之前输入新的 IP 地址。比如,为了监听所有服务器的公开 IP 地址(运行 Vagrant 或想要向网络上的其他计算机展示成果时很有用),使用如下命令。

python manage.py runserver 0:8000

0 是 0.0.0.0 的简写。

用于开发的简易服务器在需要的情况下会对每一次的访问请求重新载入一遍 Python 代码,所以不需要为了让修改的代码生效而频繁地重新启动服务器。然而,一些动作,比如添加新文件,将不会触发自动重新加载,这时得手动重启服务器。

3) 创建投票应用程序

现在开发环境配置好了,可以开始创建应用程序。

在 Django 中,每一个应用程序都是一个 Python 包,并且遵循着相同的约定。Django 自带一个工具,可以帮助生成应用程序的基础目录结构,这样就能专心写代码,而不用专注于创建目录了。

应用程序和项目有什么区别?

应用程序是一个专门做某件事的网络程序,比如博客系统、公共记录的数据库,或者简单的投票应用程序等。项目则是一个网站使用的配置和应用程序的集合。项目可以包含很多个应用程序,应用程序可以被很多个项目使用。

用户的应用程序可以存放在任何 Python path 定义的路径中。在 manage.py 同级目录下创建投票应用程序,这样它就可以作为顶级模块导入,而不是作为 mysite 的子模块。

请确定应用程序现在处于 manage.py 所在的目录下,然后运行以下命令创建一个应用程序。

python manage.py startapp polls

这将会创建一个 polls 目录,它的目录结构大致如下:
polls/
    _ _init_ _.py
    admin.py
    apps.py
    migrations/
        _ _init_ _.py
    models.py
    tests.py
    views.py
这个目录结构包括投票应用程序的全部内容。

4) 实现第一个视图

下面开始编写代码实现第一个视图。打开 polls/views.py,输入以下代码。
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")
这是 Django 中较简单的视图。如果想看见效果,需要将一个 URL 映射到它——这就是需要 URLconf(URL 配置)的原因。

为了创建 URLconf,请在 polls 目录里新建一个 urls.py 文件。此处的应用程序目录现在看起来应该是如下这样的。
polls/
    _ _init_ _.py
    admin.py
    apps.py
    migrations/
        _ _init_ _.py
    models.py
    tests.py
    urls.py
    views.py
在 polls/urls.py 中,输入如下代码:
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
下一步是要在根 URLconf 文件中指定创建的 polls.urls 模块。在 mysite/urls.py 文件的 urlpatterns 列表里插入一个 include 函数,如下所示。
from django.contrib import admin
from django.urls import include, path
 
urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]
函数 include 允许引用其他 URLconf。每当 Django 遇到 include 时,它会截断与此项匹配的 URL 的部分,并将剩余的字符串发送到 URLconf 以供进一步处理。

设计 include 的理念是使其可以即插即用。投票应用程序有它自己的 URLconf(polls/urls.py),它们被放在 /polls/、/fun_polls/、/content/polls/,或者其他任何路径下,都能够正常工作。

应何时使用 include?当项目包括其他 URL 模式时应该总是使用 include,admin.site.urls 是唯一例外。

现在把 index 视图添加进了 URLconf。接下来通过以下命令启动服务器。

python manage.py runserver

用浏览器访问 http://127.0.0.1:8000/polls/,应该能够看见 “Hello, world. You're at the polls index.”,这是在 index 视图中定义的,如图5所示。

访问
图5:访问

函数 path 有4个参数,其中2个为必需参数——route 和 view,2个为可选参数——kwargs 和 name。现在,是时候来研究这些参数的含义了。

① 参数route

route 是一个匹配 URL 的准则(类似正则表达式)。当 Django 响应一个请求时,它会从 urlpatterns 的第一项开始,按顺序依次匹配列表中的项,直到找到匹配的项。

这个准则不会匹配 GET 和 POST 参数或域名,例如:

② 参数view

当 Django 找到了一个匹配准则时,就会调用特定的视图函数,并传入一个 HttpRequest 对象作为第一个参数,被“捕获”的参数以关键字参数的形式传入。后文会给出一个例子。

③ 参数kwargs

任意一个关键字参数可以作为一个字典传递给目标视图函数,此处不会使用这一特性。

④ 参数name

为 URL 起名能让用户在 Django 的任意地方唯一地引用它,尤其是在模板中。这个有用的特性允许用户只修改一个文件就能全局地修改某个 URL 模式。

模型和Admin站点

接下来将讲解建立数据库,用户可以创建第一个模型,并主要关注 Django 提供的自动生成的管理页面。

1) 数据库配置

现在,打开 mysite/settings.py,这是一个包含 Django 项目设置的 Python 模块。

通常,这个配置文件使用 SQLite 作为默认数据库。如果不熟悉数据库,或者只是想尝试使用 Django,那么这是最简单的选择之一。Python 中内置了 sqlite3 模块,所以用户无须安装即可使用它。

当开始进行一个真正的项目时,用户可能更倾向使用一个更具扩展性的数据库,例如 PostgreSQL,以避免中途切换数据库。

如果用户想使用其他数据库,需要安装合适的 databasebindings,然后改变设置文件中 DATABASES 中的一些键值。

ENGINE 为数据库引擎,可选值有很多,比如 django.db.backends.sqlite3、django.db.backends.postgresq、django.db.backends.mysql、django.db.backends.oracle。

NAME 代表数据库的名称。如果使用的是 SQLite,数据库将是计算机上的一个文件,在这种情况下,NAME 应该是此文件的绝对路径,包括文件名。默认值 os.path.join(BASE_DIR,'db.sqlite3') 将会把数据库文件储存在项目的根目录。

如果不使用 SQLite,则必须添加一些额外设置,比如 USER、PASSWORD、HOST 等。
# SQLite
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'mydatabase',
    }
}
 
# MySQL
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydatabase',
        'USER': 'mydatabaseuser',
        'PASSWORD': 'mypassword',
        'HOST': '127.0.0.1',
        'PORT': '5432',
    }
}
编辑 mysite/settings.py 文件前,先设置 TIME_ZONE 为目前所处的时区。

此外,需关注文件头部的 INSTALLED_APPS 设置项,这里包括会在项目中启用的所有 Django 应用程序。这些应用程序能在多个项目中使用,也可以打包并且发布应用程序,让别人使用它们。

通常,INSTALLED_APPS 默认包括以下 Django 自带的应用程序。
这些应用程序被默认启用是为了给常规项目提供方便。

默认启用的某些应用程序、需要至少一个数据表,所以,在使用它们之前需要在数据库中创建一些表。请运行以下命令。

python manage.py migrate

这个 migrate 命令用于检查 INSTALLED_APPS 设置,为其中的每个应用程序创建需要的数据表,至于具体会创建什么,这取决于 mysite/settings.py 设置文件和每个应用程序的数据库迁移文件。

就像之前介绍的,为了方便大多数项目,系统默认激活了一些应用程序,但并不是每个人都需要它们。如果不需要某个或某些应用程序,可以在运行 migrate 命令前毫无顾虑地从 INSTALLED_APPS 里注释或者删除它们。

migrate 命令只会为在 INSTALLED_APPS 里声明了的应用程序进行数据库迁移。

2) 创建模型

在 Django 里编写一个数据库驱动的 Web 应用程序的第一步是定义模型,也就是进行数据库结构设计和定义附加的其他元数据。

在这个简单的投票应用程序中,需要创建两个模型:问题(Question)模型和选项(Choice)模型。
这些概念可以通过一个简单的 Python 类来描述。按照下面的例子来编辑 polls/models.py 文件。
from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
代码非常“直白”。每个模型都被表示为 django.db.models.Model 类的子类。每个模型有一些类变量,它们都表示模型里的数据库字段。

每个字段都是 Field 类的实例,比如字符字段被表示为 CharField,日期时间字段被表示为 DateTimeField。这将告诉 Django 每个字段要处理的数据类型。

每个 Field 类实例变量的名字(例如 question_text或 pub_date)也是字段名,所以最好使用对“机器友好”的格式。用户将会在 Python 代码里使用它们,而数据库会将它们作为列名。

用户可以使用可选的选项来为 Field 定义一个人类可读的名字。这个功能在很多 Django 内部组成部分中都被使用了,而且作为文档功能的一部分。如果某个字段没有提供此名字,Django 将会使用对“机器友好”的名字,也就是变量名。

定义某些 Field 类实例需要参数。例如 CharField 需要一个 max_length 参数,这个参数不仅用于定义数据库结构,也用于验证数据,后文将会介绍这方面的内容。

Field 也能够接收多个可选参数。在上面的例子中,可以看到将 votes的default(也就是默认值)设为 0。

注意,上述例子还使用 ForeignKey 定义了一个关系。这将告诉 Django 每个 Choice 对象都关联到一个 Question 对象。Django 支持常用的数据库关系:多对一、多对多和一对一。

3) 激活模型

上面用于创建模型的代码提供给 Django 很多信息,通过这些信息,Django 可以:
但是首先得把投票应用程序安装到项目里。

为了在项目中包含这个应用程序,需要在配置类 INSTALLED_APPS 中添加设置。因为 PollsConfig 类写在文件 polls/apps.py 中,所以它的点式路径是 polls.apps.PollsConfig。

在文件 mysite/settings.py 中为 INSTALLED_APPS子 项添加点式路径后,它看起来如下所示。
INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
现在 Django 项目会包含投票应用程序。接着运行下面的命令。

python manage.py makemigrations polls

将会看到类似下面这样的输出。
Migrations for 'polls':
  polls/migrations/0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice
再次运行 migrate 命令,在数据库里创建新定义的模型的数据表。
python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Rendering model states... DONE
  Applying polls.0001_initial... OK
这个 migrate 命令选中所有还没有运行过的迁移并应用在数据库上,也就是对模型的更改同步到数据库结构上。

Django 通过在数据库中创建一个特殊的表 django_migrations 来跟踪运行过哪些迁移。

迁移是非常强大的功能,它能让用户在开发过程中持续改变数据库结构而不需要重新删除和创建表,它专注于使数据库平滑升级而不会丢失数据。

我们会在后文更加深入地讲解这部分内容,现在只需要记住,改变模型需要以下3步。
数据库迁移被分解成生成和应用两个命令是为了能够在代码控制系统上提交迁移数据并使其能在多个应用程序里使用;这不仅会让开发变得更加简单,也给别的开发人员和生产环境中的使用带来了方便。

4) 初试API

现在讲解进入 Python 命令行,尝试一下 Django 为用户创建的各种 API。通过以下命令打开 Python 命令行。

python manage.py shell

使用这个命令而不是简单地使用 Python 是因为 manage.py 会设置 DJANGO_SETTINGS_MODULE 环境变量,这个变量会让 Django 根据 mysite/settings.py 文件来设置 Python 包的导入路径。

当成功进入命令行后,来试试 database API 吧。
>>> from polls.models import Choice, Question  # Import the model classes we just wrote.
 
# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>
 
# Create a new Question.
# Support for time zones is enabled in the default settings file.
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
 
# Save the object into the database. You have to call save() explicitly.
>>> q.save()
 
# Now it has an ID.
>>> q.id
1
 
# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)
 
# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()
 
# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>
Question:Question object(1) 对于了解这个对象的细节没什么帮助。可以通过编辑 Question 模型的代码(位于 polls/models.py中)来修复这个问题。

给 Question 和 Choice 增加 _ _str_ _ 方法。
from django.db import models
 
class Question(models.Model):
    # ...
    def _ _str_ _(self):
        return self.question_text
 
class Choice(models.Model):
    # ...
    def _ _str_ _(self):
        return self.choice_text
给模型增加 _ _str_ _ 方法是很重要的,这能给用户在命令行里使用模型带来方便。Django 自动生成的admin也使用这个方法来表示对象。

再为此模型添加一个自定义方法。
import datetime
 
from django.db import models
from django.utils import timezone
 
 
class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
新加入的 import datetime 和前文提到的 from django.utils import timezone 分别导入了 Python 中的标准 datetime 模块和 Django 中与时区相关的 django.utils.timezone 模块。如果你不太熟悉 Python 中的时区处理,可以看一看时区支持文档。

保存文件,然后通过 python manage.py shell 命令打开 Python 命令行。
>>> from polls.models import Choice, Question
 
# Make sure our _ _str_ _() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>
 
# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text_ _startswith='What')
<QuerySet [<Question: What's up?>]>
 
# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date_ _year=current_year)
<Question: What's up?>
 
# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.
 
# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>
 
# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True
 
# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)
 
# Display any choices from the related object set -- none so far.
>>> q.choice_set.all()
<QuerySet []>
 
# Create three choices.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
 
# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>
 
# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3
 
# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question_ _pub_date_ _year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
 
# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text_ _startswith='Just hacking')
>>> c.delete()

5) 介绍 Django 管理页面

首先,注册一个能登录管理页面的账户。请运行下面的命令:

python manage.py createsuperuser

输入想要使用的账户名并按 Enter 键。

Username: admin

然后会提示用户输入想要使用的电子邮箱地址。

E-mail address: admin@example.com

最后一步是输入密码。用户会被要求输入两次密码,第二次输入的目的是确认第一次输入的确实是用户想要的密码。

Password: **********
Password (again): *********
Superuser created successfully.

Django 的管理页面默认是启用的。启动开发服务器,来看看它到底是什么样的。

如果开发服务器未启动,用以下命令启动它。

python manage.py runserver

现在,打开浏览器,转到本地域名的 /admin/ 目录,比如 http://127.0.0.1:8000/admin/,会显示管理员登录页面,如图6所示。

Django 管理员登录页面
图6:Django 管理员登录页面

现在,试着使用已经注册的超级用户来登录,会看到 Django 管理页面的索引页,如图7所示。

Django 管理页面的索引页1
图7:Django 管理页面的索引页1

用户将会看到几种可编辑的内容:组和用户。它们是由 django.contrib.auth 提供的,这是 Django 开发的认证框架。

但是投票应用程序在哪儿呢?它没在索引页里显示。

这时,用户只需要做一件事:告诉管理页面,Question 对象需要被管理。打开 polls/admin.py 文件,把它编辑成下面这样。
from django.contrib import admin
from .models import Question

admin.site.register(Question)
现在向管理页面注册了 Question 类。Django 知道它应该被显示在索引页,管理页面的索引页如图8所示。

jango 管理页面的索引页2
图8:Django 管理页面的索引页2

单击【Questions】,可以看到是 Questions 对象的列表 change list。这个页面会显示所有数据库里的 Question对象,可以选择一个来修改。

这里有在前文创建的“What's up?”问题,如图9所示。

Question 对象的列表
图9:Question 对象的列表

单击【What's up?】来编辑这个 Question 对象,如图10所示。

编辑 Question 对象
图10:编辑 Question 对象

要注意如下事项:
页面的底部提供了以下几个选项:
如果显示的发布日期(Date published)和在前文创建的时间不一致,这意味着可能没有正确设置 TIME_ZONE。改变设置,然后重新载入页面看一看是否显示了正确的值。

通过单击【今天(Today)】和【现在(Now)】按钮改变“发布日期(Date published)”。然后单击【保存并继续编辑(Save and add another)】按钮。再单击右上角的【历史(HISTORY)】按钮,会看到一个列出了所有通过 Django 管理页面对当前对象进行修改的页面,其中列出了时间戳和进行修改操作的用户名,历史记录如 图11所示。

历史记录
图11:历史记录

视图和模板

Django 中的视图是指一类具有相同功能和模板的网页的集合。比如在一个博客中,可能会创建如下视图:
而在投票应用程序中,需要下列视图:
在 Django 中,网页和其他内容都是从视图派生而来的,每一个视图表现为一个简单的 Python 函数(或者说方法,如果是在基于类的视图里的话)。Django 将会根据用户请求的 URL 来选择使用哪个视图。

更准确地说,是根据URL中域名之后的部分进行选择。

上网时,你很可能看见过这样的 URL:

ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B

别担心,Django 里的 URL 要比这“优雅”得多!

URL 模式定义了某种 URL 的基本格式,如 /newsarchive/<year>/<month>/。

为了将 URL 和视图关联起来,Django 使用 URLconf 来配置。URLconf 将 URL 模式映射到视图。

1) 编写更多视图

向 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/34/,Django 将会运行 detail 方法并且展示 URL 里提供的问题 ID。再试一试 /polls/34/vote/,将会看到暂时用于占位的结果和投票页。

当出现请求网站的某一页面时,比如说 /polls/34/,Django 将会载入 mysite.urls 模块,因为这在配置项 ROOT_URLCONF 中设置了。然后 Django 寻找 urlpatterns 变量并且按序匹配正则表达式。在找到匹配项 polls/ 后,它切掉了匹配的文本 polls/,将剩余文本 34/,发送至 polls.urls 做进一步处理。在这里剩余文本匹配了 <int:question_id>/,使得 Django 以如下形式调用 detail 方法。

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

question_id=34 由 <int:question_id> 匹配生成。使用角括号“捕获”这部分 URL,且以关键字参数的形式发送给视图函数。上述字符串的 :question_id> 部分定义了将被用于区分匹配模式的变量名,而 int: 则是一个转换器,决定了应该以什么变量类型匹配这部分的 URL。

2) 编写一个真正有用的视图

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

用户的视图可以从数据库里读取记录,可以使用一个模板引擎(比如 Django 自带的,或者其他第三方的),可以生成一个 PDF 文件,可以输出一个 XML 文件,可以创建一个 ZIP 文件。

Django 只要求返回一个 HttpResponse,或者抛出一个异常。

因为 Django 自带的数据库 API 很方便,所以可以试一试在视图里使用它。在 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)
 
# Leave the rest of the views (detail, results, vote) unchanged
这里有个问题,页面的设计由视图函数的代码实现。如果想改变页面的样子,就需要编辑 Python 代码。这里使用 Django 的模板,只要创建一个视图,就可以将页面的设计从代码中“分离”出来。

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

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

这就是为什么尽管没有像在前文中介绍的那样修改 DIRS 设置,Django 也能正确找到 polls 的模板位置。

在刚刚创建的 templates 目录里,再创建一个目录 polls,然后在其中新建一个文件 index.html。换句话说,模板文件所在的路径应该是 polls/templates/polls/index.html。因为 Django 会寻找到对应的 app_directories,所以只需要使用 polls/index.html 就可以引用这一模板。

将下面的代码输入刚刚创建的模板文件。
{% 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 %}
然后,更新 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/ 将会看见一个无序列表,列出了添加的“What's up?”投票问题,链接指向这个投票的详情页。

载入模板,填充上下文,再返回由它生成的 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)
我们注意到,这里不再需要导入 loader 和 HttpResponse。不过如果还有其他函数(比如 detail、results 和 vote)需要用到 HttpResponse,就需要保持 HttpResponse 的导入。

3) 抛出 Http404 异常

现在来处理投票详情视图,它会显示指定投票的问题标题。下面是这个视图的代码。
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})
这里有一个原则。如果指定问题 ID 所对应的问题不存在,那么这个视图就会抛出一个 Http404 异常。

4) 使用模板系统

下面回过头来看看详情视图。它向模板传递了上下文变量 question。下面是 polls/detail.html 模板里正式的代码。
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
模板系统统一使用点符号来访问变量的属性。在示例 {{question.question_text}} 中,Django 尝试对 Question 对象使用字典查找(也就是使用 obj.get(str) 操作),如果失败了就尝试属性查找(也就是 obj.str 操作),结果是成功了。如果这一操作也失败的话,将会尝试列表查找(也就是 obj[int] 操作)。

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

5) 去除模板中的硬编码 URL

在 polls/index.html 里编写投票链接的代码,链接是硬编码的。

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

问题在于,硬编码和强耦合的链接,对于一个包含很多应用程序的项目来说,修改起来是十分困难的。然而,因为在 polls.urls 的 url 函数中通过 name 参数为 URL 定义了名字,所以可以使用 {%url%} 标签代替它。

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

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

...
# the 'name' value as called by the {% url %} template tag
path('<int:question_id>/', views.detail, name='detail'),
...

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

...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...

6) 为URL添加命名空间

该项目只有一个投票应用程序。在一个真实的 Django 项目中,可能会有 5 个、10 个、20 个,甚至更多的应用程序。Django 如何分辨重名的URL呢?

举个例子,投票应用程序有详情视图,可能另一个博客应用程序也有同名的视图。Django 如何知道 {%url%} 标签到底对应哪一个应用程序的 URL 呢?

答案是:在根 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>

推荐阅读