Django学习教程

通过Django教程,创建一个基本的投票应用程序。它将由两部分组成:
▶ 一个让人们查看和投票的公共站点。
▶ 一个让你能添加、修改和删除投票的管理站点。
注意:本教程适用于Django4.1,开发软件Pycharm、Anaconda。

Django学习教程

一、安装Django

二、创建Anaconda虚拟环境

2.1 打开Anaconda Prompt

在这里插入图片描述

2.2 创建虚拟环境

创建一个新的虚拟环境,路径为F:\PyTorch\Django_py3.9,python版本为3.9

conda create --prefix=F:\PyTorch\Django_py3.9 python=3.9 #创建虚拟环境
conda activate F:\PyTorch\Django_py3.9 #激活环境
2.3 在虚拟环境中安装django
conda install django
2.4 查看django安装版本,版本为4.1
python -m django --version

三、创建Django项目

3.1 使用pycharm的Terminal命令行,切换虚拟环境
  1. 打开pycharm,点击File → open,打开django项目
  2. 点击左下角的Terminal命令行
    在这里插入图片描述
  3. 查看虚拟环境,输入conda env list或conda info --envs;切换成含有django包的虚拟环境;
    在这里插入图片描述
  4. 这时,可以发现不能切换,通过如下修改,可以顺利切换虚拟环境;
    在这里插入图片描述
    设置完毕
  5. 输入 activate XXX(虚拟环境名) 进入,如果忘记了虚拟环境的名字,可通过步骤3查看
    在这里插入图片描述
3.2 设置项目的存放路径
  1. 打开Terminal命令行,cd 到一个你想放置你代码的目录。
    cd F:\PycharmProjects\django
  2. 运行以下命令,创建项目,项目名为vote
    django-admin startproject vote
  3. 创建完项目vote,此时的项目结构为
    vote/ # 根目录,项目的容器
    	manage.py # 用各种方式管理 Django 项目的命令行工具
        vote/ 包含你的项目,它是一个纯 Python 包。它的名字就是当你引用它内部任何东西时需要用到的 Python 包名
            __init__.py # 一个空文件,告诉 Python 这个目录应该被认为是一个 Python 包
            settings.py # 项目的配置文件
            urls.py # 项目的 URL 声明,就像网站的“目录”
            asgi.py # 项目运行在 ASGI 兼容的 Web 服务器上的入口
            wsgi.py # 项目运行在 WSGI 兼容的Web服务器上的入口
    
3.3 确认项目是否创建成功
  1. 确定Terminal命令行的目录是外层的vote目录,如果不是,就cd到此目录。

  2. 运行以下命令,启动服务器。

    python manage.py runserver
    
  3. 如果出现下图,说明服务器启动成功,并且正在运行。

    启动后,默认使用SQLite数据库,默认在项目根目录下创建一个db.sqlite3的文件数据库。此时的数据库没有数据表,可以在settings.py里指定数据库的文件的路径,也可以更改为其他数据库引擎,达到访问其他数据的目的,数据库的配置可随时替换。

    在这里插入图片描述

  4. 通过浏览器访问 http://127.0.0.1:8000/ 。你将看到一个“祝贺”页面,有一只火箭正在发射。
    在这里插入图片描述

  5. 如果要更换端口号,可以输入以下命令python manage.py runserver XXXX(端口号)。不需更换,请忽略此步骤。

    python manage.py runserver 1024
    

    在这里插入图片描述

  6. 如果要修改服务器监听的IP,输入以下命令。不需修改,请忽略此步骤。

    python manage.py runserver 0.0.0.0:8000
    

四、创建投票应用

4.1 项目 VS 应用(APP)
  • APP是一个专门做某件事的网络应用程序——比如博客系统,或者公共记录的数据库,或者小型的投票程序。项目则是一个网站使用的配置和APP的集合。
  • 项目可以包含很多个APP。APP可以被很多个项目使用。
  • APP可以有各自独立的表结构、函数、HTML模板、css。一般开发简洁,一个项目创建一个APP即可。
4.2 创建APP-polls
  1. 打开Terminal命令行,cd到外层的vote目录下,输入以下命令python manage.py startapp XXX(应用名)创建APP。

    python manage.py startapp polls
    
  2. polls 目录,它的目录结构大致如下图所示,这个目录结构包括了投票应用的全部内容。

    polls/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        views.py
    
4.3 编写第一个视图,函数include()

视图函数:具体的逻辑处理函数,用来接受web请求,并返回web响应。(编写页面)
▶ URL(创建页面关系)
▶ path(route, view, kwargs, name)函数的4个参数,route是包含URL模式的字符串;view会调用指定的视图函数,将HttpRequest对象作为第一个参数,并将路由中的任何捕获值作为关键字参数;其他两个参数可选

  1. 打开 polls/views.py,输入以下代码创建视图。

    from django.http import HttpResponse
    
    def index(request):
        return HttpResponse("你好,这里是投票页面。")
    
  2. 编写URL与视图函数对应关系,为了创建 URLconf,在polls目录下新建 urls.py,如下图所示。
    在这里插入图片描述

  3. 在 polls/urls.py 中,输入如下代码。

    from django.urls import path
    from . import views
    
    urlpatterns = [
        path('', views.index, name='index'),
    ]
    
  4. 在 vote/urls.py 文件的 urlpatterns 列表里插入一个 include(), 如下所示。

    函数 include() 允许引用其它 URLconfs。每当 Django 遇到 include() 时,它会截断与此项匹配的 URL 的部分,并将剩余的字符串发送到 URLconf 以供进一步处理。

    	from django.contrib import admin
    	from django.urls import include, path
    	
    	urlpatterns = [
    	    path('polls/', include('polls.urls')),
    	    path('admin/', admin.site.urls),
    	]
    
  5. 打开Terminal命令行,通过以下命令启动服务器,验证是否正常工作。(在命令行输入ctrl+c,结束服务器运行)

    python manage.py runserver
    
  6. 使用浏览器访问 http://localhost:8000/polls/ ,能够看见 “你好,这里是投票页面。”,则运行成功。
    在这里插入图片描述

4.4 数据库配置
  1. 打开 vote/settings.py,这是个包含了 Django 项目设置的 Python 模块。
    在这里插入图片描述

  2. 改变设置文件中 DATABASES ‘default’ 项目中的一些键值。如果使用的是SQLite数据库,此步骤请忽略。在这里插入图片描述

    ▶ ENGINE – 可选值有’django.db.backends.sqlite3’,
    ‘django.db.backends.postgresql’,
    ‘django.db.backends.mysql’,或
    ‘django.db.backends.oracle’。
    ▶ NAME – 数据库的名称。如果你使用
    SQLite,数据库将是你电脑上的一个文件,在这种情况下,NAME 应该是此文件完整的绝对路径,包括文件名。默认值 BASE_DIR / ‘db.sqlite3’ 将把数据库文件储存在项目的根目录。 如果不使用 SQLite,则必须添加一些额外设置,比如 USER 、PASSWORD 、 HOST 等等。

  3. 查看一下 vote/settings.py 内的 INSTALLED_APPS 设置项。

    这里包括了会在项目中启用的所有 Django 应用。应用能在多个项目中使用,也可以打包并且发布应用,让别人使用它们。通常, INSTALLED_APPS 默认包括了以下 Django 的自带应用:
    ▶ django.contrib.admin – 管理员站点, 你很快就会使用它。
    ▶ django.contrib.auth – 认证授权系统。
    ▶ django.contrib.contenttypes – 内容类型框架。
    ▶ django.contrib.sessions – 会话框架。
    ▶ django.contrib.messages – 消息框架。
    ▶ django.contrib.staticfiles – 管理静态文件的框架。
    默认启用以上应用,为了给常规项目提供方便。

  4. 由于默认开启的某些应用需要至少一个数据表,所以在使用他们之前需要在数据库中创建一些表。在Terminal命令行输入以下命令查看 INSTALLED_APPS 配置,并根据 vote/settings.py 文件中的数据库配置和随应用提供的数据库迁移文件。
    python manage.py migrate

4.5 创建模型

在 Django 里写一个数据库驱动的 Web 应用的第一步是定义模型 - 也就是数据库结构设计和附加的其它元数据。

在这个投票应用中,需要创建两个模型:问题 Question 和选项 Choice

  • Question 模型包括问题描述和发布时间。
  • Choice 模型有两个字段,选项描述和当前得票数。每个选项属于一个问题。
  • 打开 polls/models.py,输入以下代码创建模型。
    from django.db import models
    
    # 每个模型被表示为 django.db.models.Model 类的子类。
    # 每个模型有许多类变量,它们都表示模型里的一个数据库字段。
    # 每个字段都是 Field 类的实例。比如,字符字段被表示为 CharField ,日期时间字段被表示为 DateTimeField
    # 每个 Field 类实例变量的名字(如 question_text)是字段名,而数据库会将它们作为列名。
    # Field 能够接收多个可选参数,如default、max_length
    
    class Question(models.Model):
        question_text = models.CharField(max_length = 200)
        # pub_date使用可选参数'date published'来为 Field 定义一个人类可读的列名。如果某个字段没有提供列名,Django将会使用变量名作为列名。
        pub_date = models.DateTimeField('date published')
    
    class Choice(models.Model):
        # 使用 ForeignKey 定义了一个关系。这将告诉 Django,每个 Choice 对象都关联到一个 Question 对象。
        # Django 支持所有常用的数据库关系:多对一、多对多和一对一。
        question = models.ForeignKey(Question, on_delete = models.CASCADE)
    
        Choice_text = models.CharField(max_length = 200)
        vote = models.IntegerField(default = 0)
    
4.6 激活模型

上面的一小段用于创建模型的代码给了 Django 很多信息,通过这些信息,Django 可以:
▶ 为这个APP创建数据库 schema(生成 CREATE TABLE 语句)。
▶ 创建可以与 Question 和 Choice 对象进行交互的 Python 数据库 API。
但是首先得通过以下步骤把 polls 应用安装到项目里。

  1. 打开vote/settings.py, 找到 INSTALLED_APPS 设置项,添加如图所示代码。

  2. 输入以下命令python manage.py makemigrations polls,Django会检测你对模型文件的修改(在这种情况下,你已经取得了新的),并且把修改的部分储存为一次迁移

    ▶ 迁移是非常强大的功能,它能让开发者在开发过程中持续的改变数据库结构而不需要重新删除和创建表,专注于使数据库平滑升级而不会丢失数据。
    ▶ 迁移是 Django 对于模型定义(也就是项目的数据库结构)的变化的储存形式 ,它们其实也只是一些磁盘上的文件。模型的迁移数据,它被储存在 polls/migrations/0001_initial.py 里。不需每次都阅读迁移文件,它们被设计成人类可读的形式,是为了便于开发者手动调整 Django 的修改方式。(Django 有一个自动执行数据库迁移并同步管理你的数据库结构的命令 - 这个命令是 migrate。)
    ▶ 改变模型需要这三步:
    1.编辑 models.py 文件,改变模型。
    2.运行 python manage.py makemigrations 为模型的改变生成迁移文件。
    3.运行 python manage.py migrate 来应用数据库迁移。
    数据库迁移被分解成生成和应用两个命令是为了让你能够在代码控制系统上提交迁移数据并使其能在多个应用里使用,简单方便。

    在这里插入图片描述

  3. 看看迁移命令会执行哪些 SQL 语句。使用sqlmigrate 命令接收一个迁移的名称,然后返回对应的 SQL。
    python manage.py sqlmigrate polls 0001

    返回结果如下:

    BEGIN;
    --
    -- Create model Question
    --
    CREATE TABLE "polls_question" (
    	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
    	"question_text" varchar(200) NOT NULL, 
    	"pub_date" datetime NOT NULL
    );
    --
    -- Create model Choice
    --
    CREATE TABLE "polls_choice" (
    	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 	
    	"Choice_text" varchar(200) NOT NULL, 
    	"vote" integer NOT NULL, 
    	"question_id" bigint NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERRED
    );
    CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
    COMMIT;
    
  4. 再次执行命令python manage.py migrate ,在数据库里创建新定义的模型的数据表这个 migrate 命令选中所有还没有执行过的迁移(Django 通过在数据库中创建一个特殊的表 django_migrations 来跟踪执行过哪些迁移)并应用在数据库上 - 也就是将模型的更改同步到数据库结构上。
    在这里插入图片描述

4.7 初试API
  1. 打开Terminal命令行,尝试一下 Django 创建的各种 API。输入以下命令进入交互式 Python 命令行。
    python manage.py shell
    在这里插入图片描述

  2. 进入后,试试数据库API,完成后输入exit()退出交互。

    >>> 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, so
    # 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(2023, 1, 22, 3, 1, 43, 775217, tzinfo=datetime.timezone.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)>]>
    
  3. 打开 polls/models.py,给模型Question、Choice添加 str() 方法,不仅在命令行使用方便,Django自动生成的admin也用此方法表示对象。
    在这里插入图片描述

  4. 再为模型添加一个自定义方法was_published_recently()。
    在这里插入图片描述

  5. 保存文件后,通过python manage.py shell命令再次打开 Python 交互式命令行,运行以下命令。

    >>> from polls.models import Choice, Question
    # 确保 __str__() 添加有效
    >>> Question.objects.all()
    <QuerySet [<Question: What's up?>]>
    
    # Django 提供了一个完全由关键字参数驱动的丰富的数据库查找 API
    >>> Question.objects.filter(id=1)
    <QuerySet [<Question: What's up?>]>
    >>> Question.objects.filter(question_text__startswith='What')
    <QuerySet [<Question: What's up?>]>
    
    # 获取今年发布的问题
    >>> from django.utils import timezone
    >>> current_year = timezone.now().year
    >>> Question.objects.get(pub_date__year=current_year)
    <Question: What's up?>
    
    # 请求一个不存在的 ID,这将引发异常
    >>> Question.objects.get(id=2)
    Traceback (most recent call last):
        ...
    DoesNotExist: Question matching query does not exist.
    
    # 通过主键查找是最常见的情况,因此 Django 提供了一种主键精确查找的快捷方式。
    # 以下与 Question.objects.get(id=1) 相同。
    >>> Question.objects.get(pk=1)
    <Question: What's up?>
    
    # 确定自定义方法有效
    >>> q = Question.objects.get(pk=1)
    >>> q.was_published_recently()
    True
    
    # 给问题几个选项。 create 调用构造一个新的 Choice 对象,
    # 执行 INSERT 语句,将选项添加到可用选项集中并返回新的 Choice 对象。
    # Django 创建一个集合来保存可以通过 API 访问的 ForeignKey 关系的“另一端”(例如问题的选择)。
    >>> q = Question.objects.get(pk=1)
    
    # 显示相关对象集中的任何选项——目前还没有。
    >>> q.choice_set.all()
    <QuerySet []>
    
    # 创建三个选项
    >>> 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对象具有对其相关Question对象的 API 访问权限。
    >>> c.question
    <Question: What's up?>
    
    # 反之亦然:问题对象可以访问选择对象。
    >>> q.choice_set.all()
    <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
    >>> q.choice_set.count()
    3
    
    # API会根据您的需要自动遵循关系。
    #使用两个下划线来分隔关系。这适用于你想要的深度;没有限制。
    #查找 pub_date 在今年的任何问题的所有选项
    #(重用在上面创建的“current_year”变量)。
    >>> Choice.objects.filter(question__pub_date__year=current_year)
    <QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
    
    # 删除其中一个选项
    >>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
    >>> c.delete()
    

五、Django管理员页面

5.1 创建一个管理员账号
  1. 运行下面的命令,创建一个能登录管理页面的用户。
    python manage.py createsuperuser
  2. 键入你想要使用的用户名,然后按下回车键Username: admin
  3. 然后提示你输入想要使用的邮件地址:Email address: admin@example.com
  4. 最后一步是输入密码。你会被要求输入两次密码,第二次的目的是为了确认第一次输入的确实是你想要的密码。
    Password: **********
    Password (again): *********
    Superuser created successfully.
5.2 启动开发服务器

Django 的管理界面默认就是启用的。

  1. 用以下命令启动开发服务器。python manage.py runserver

  2. 打开浏览器,转到http://127.0.0.1:8000/admin/,进入管理员登录界面。因为翻译功能默认是开启的,如果设置了 LANGUAGE_CODE,登录界面将显示你设置的语言(如果 Django 有相应的翻译)

    在这里插入图片描述

5.3 进入管理站点页面

使用上一步创建的管理员账户登录,进入Django管理页面的索引页。你将会看到几种可编辑的内容:组和用户。它们是由 django.contrib.auth 提供的,这是 Django 开发的认证框架。
在这里插入图片描述

5.4 向管理页面中加入投票应用

投票应用没在索引页面里显示。这是需要向 polls/admin.py 输入以下代码,给问题 Question 对象一个后台接口。

from django.contrib import admin
from .models import Question

admin.site.register(Question)
5.5 体验便捷的管理功能

点击 “Questions” 。现在看到是问题 “Questions” 对象的列表 “change list” 。这个界面会显示所有数据库里的问题 Question 对象,可以选择一个来修改。这里现在有之前创建的 “What’s up?” 问题。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

▶ 注意事项:
这个表单是从问题 Question 模型中自动生成的
不同的字段类型(日期时间字段 DateTimeField 、字符字段 CharField)会生成对应的 HTML 输入控件。每个类型的字段都知道它们该如何在管理页面里显示自己。
每个日期时间字段 DateTimeField 都有 JavaScript 写的快捷按钮。日期有转到今天(Today)的快捷按钮和一个弹出式日历界面。时间有设为现在(Now)的快捷按钮和一个列出常用时间的方便的弹出式列表。
▶ 页面的底部提供了几个选项:
——保存(Save) - 保存改变,然后返回对象列表。
——保存并继续编辑(Save and continue editing) - 保存改变,然后重新载入当前对象的修改界面。
——保存并新增(Save and add another) - 保存改变,然后添加一个新的空对象并载入修改界面。
——删除(Delete) - 显示一个确认删除页面。
▶ 如果显示的 “发布日期(Date Published)” 和之前创建它们的时间不一致,可能是没有正确的设置 TIME_ZONE 。改变设置,然后重新载入页面看看是否显示了正确的值。
▶ 通过点击 “今天(Today)” 和 “现在(Now)” 按钮改变 “发布日期(Date Published)”。然后点击 “保存并继续编辑(Save and add another)”按钮。然后点击右上角的 “历史(History)”按钮。你会看到一个列出了所有通过 Django 管理页面对当前对象进行的改变的页面,其中列出了时间戳和进行修改操作的用户名。

六、视图和模板

6.1 视图

Django 中的视图的概念是「一类具有相同功能和模板的网页的集合」。比如,
▶ 在一个博客应用中,你可能会创建如下几个视图:
博客首页——展示最近的几项内容。
内容“详情”页——详细展示某项内容。
以年为单位的归档页——展示选中的年份里各个月份创建的内容。
以月为单位的归档页——展示选中的月份里各天创建的内容。
以天为单位的归档页——展示选中天里创建的所有内容。
评论处理器——用于响应为一项内容添加评论的操作。
▶ 投票应用中,需要下列几个视图:
问题索引页——展示最近的几个投票问题。
问题详情页——展示某个投票的问题和不带结果的选项列表。
问题结果页——展示某个投票的结果。
投票处理器——用于响应用户为某个问题的特定选项投票的操作。

在 Django 中,网页和其他内容都是从视图派生而来。每一个视图表现为一个 Python 函数(或者说方法,如果是在基于类的视图里的话)。Django 将会根据用户请求的 URL 来选择使用哪个视图(更准确的说,是根据 URL 中域名之后的部分)。
▶ URL 样式是 URL 的一般形式 - 例如:/newsarchive/year/month/。
▶ 为了将 URL 和视图关联起来,Django 使用了 ‘URLconfs’ 来配置。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)
    
  2. 把这些新视图添加进 polls/urls.py 里,添加几个 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'),
    ]
    

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

6.2 模板:函数render()

▶ 模板系统统一使用点符号来访问变量的属性。由于jinja2模板引擎是仿django模板开发的,可以学习jinja2模板基本语法来了解django模板。
▶ 其本质上,是在HTML中写了一些占位符,由数据对占位符进行替换处理。
▶函数render() :「载入模板,填充上下文,再返回由它生成的 HttpResponse 对象」
▶ 视图从数据库里读取记录,可以使用一个模板引擎(比如 Django 自带的,或者其他第三方的),可以生成一个 PDF 文件,可以输出一个 XML,创建一个 ZIP 文件,可以做任何你想做的事,使用任何你想用的 Python 库。
在这里插入图片描述

  1. 写一个真正有用的视图:在 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)
    # 保留其余视图(detail, results, vote)不变
    
  2. 在 polls 目录里创建一个 templates 目录。Django 将会在这个目录里查找模板文件。

    ▶ 模板命名空间:
    虽然可以将模板文件直接放在 polls/templates 文件夹中(而不是再建立一个 polls 子文件夹),但是Django 将会选择第一个匹配的模板文件,如果有一个模板文件正好和另一个应用中的某个模板文件重名,Django 没有办法区分它们。因此,需要帮助 Django 选择正确的模板,最好的方法就是把他们放入各自的命名空间中,也就是把这些模板放入一个和自身应用重名的子文件夹里。
    ▶ 项目的 TEMPLATES 配置项描述了 Django 如何载入和渲染模板。默认的设置文件设置了 DjangoTemplates 后端,并将 APP_DIRS 设置成了 True。这一选项将会让 DjangoTemplates 在每个 INSTALLED_APPS 文件夹中寻找 “templates” 子目录。

  3. 将下面的代码输入到刚刚创建的模板文件polls/templates/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 %}
    
  4. 更新一下 polls/views.py 里的 index 视图,使其载入 polls/index.html 模板文件,并且向它传递一个上下文(context)。这个上下文是一个字典,它将模板内的变量映射为 Python 对象。

    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))
    
  5. 使用render()函数修改代码,不再需要导入 loader 和 HttpResponse。

    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)
    
6.3 抛出 404 错误:get_object_or_404()

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

处理投票详情视图——它会显示指定投票的问题标题。

  1. 打开 polls/views.py,输入以下代码。如果指定问题 ID 所对应的问题不存在,这个视图就会抛出一个 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})
    
    
  2. 试试上面这段代码是否正常工作,在 polls/detail.html 里输入以下代码。
    {{ question }}

  3. 用 get() 函数获取一个对象,如果不存在就抛出 Http404 错误也是一个普遍的流程。Django 也提供了一个快捷函数get_object_or_404()。,下面是修改后的详情 detail() 视图代码。(也有 get_list_or_404() 函数,工作原理和 get_object_or_404() 一样,除了 get() 函数被换成了 filter() 函数。如果列表为空的话会抛出 Http404 异常。)

    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)
        return render(request, 'polls/detail.html', {'question': question})
    
6.4 使用模板系统

模板系统统一使用点符号来访问变量的属性。
▶ 模板变量:变量名必须由字母、数字、下划线(不能以下划线开头)和点组成。模板变量可以使python的内建类型,也可以是对象。
▶ 模板语法:{{变量}},{{}}中支持基本的加减乘除和逻辑运算。
▶ 模板语句:
1)for循环:
{% for item in 列表 %}
循环逻辑
{{loop.index}}表示当前是第几次循环,从1开始
{% endfor %}
2)if条件:
{% if … %}
逻辑1
{% elif … %}
逻辑2
{% else %}
逻辑3
{% endif %}
▶ 比较运算符:==、!=、<、>、<=、>=
▶ 布尔运算符:and、or、not

在detail() 视图,它向模板传递了上下文变量 question 。下面是 polls/detail.html 模板里正式的代码。

▶ 在示例 {{ question.question_text }} 中,首先 Django 尝试对 question对象使用字典查找(也就是使用 obj.get(str) 操作)
如果失败了就尝试属性查找(也就是 obj.str操作),结果是成功了。
如果这一操作也失败的话,将会尝试列表查找(也就是 obj[int] 操作)。

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

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
6.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 定义中寻具有指定名字的条目。
如果想改变投票详情视图的 URL,比如想改成 polls/specifics/12/ ,不用在模板里修改任何东西(包括其它模板),只要在 polls/urls.py 里稍微修改一下就行:path('specifics/<int:question_id>/', views.detail, name='detail'),

6.6 为 URL 名称添加命名空间

在一个真实的 Django 项目中,可能会有五个,十个,二十个,甚至更多应用。Django 如何分辨重名的 URL 呢?举个例子,polls 应用有 detail 视图,可能另一个博客应用也有同名的视图。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>

七、表单和通用视图

7.1 编写一个简单的表单
  1. 编写的投票详细页面的模板 (“polls/detail.html”) ,让它包含一个 HTML < form >元素:

    <form action="{% url 'polls:vote' question.id %}" method="post">
    <!--使用 method="post" (而不是 method="get" )是非常重要的,因为提交这个表单的行为将改变服务器端的数据。-->
    {% csrf_token %}  <!--防跨站点请求伪造-->
    <fieldset>
        <legend><h1>{{ question.question_text }}</h1></legend>
        {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
        {% for choice in question.choice_set.all %}
            <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> 
            <!--forloop.counter 指示 for 标签已经循环多少次-->
            <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
        {% endfor %}
    </fieldset>
    <input type="submit" value="投票">
    </form>
    
  2. 创建了一个 vote() 函数的虚拟实现。将下面的代码添加到 polls/views.py.

    from django.http import HttpResponse, HttpResponseRedirect
    from django.shortcuts import get_object_or_404, render
    from django.urls import reverse
    
    from .models import Choice, Question
    # ...
    def vote(request, question_id):
        question = get_object_or_404(Question, pk=question_id)
        try:
            selected_choice = question.choice_set.get(pk=request.POST['choice'])
        except (KeyError, Choice.DoesNotExist):
            # 重新显示问题投票表。
            return render(request, 'polls/detail.html', {
                'question': question,
                'error_message': "You didn't select a choice.",
            })
        else:
            selected_choice.votes += 1
            selected_choice.save()
            # 在成功处理 POST 数据后总是返回一个 HttpResponseRedirect。 
            # 如果用户点击后退按钮,这可以防止数据被发布两次。
            return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
    
  3. 当有人对 Question 进行投票后, vote() 视图将请求重定向到 Question 的结果界面。编写视图 polls/views.py,它和 detail() 视图几乎一模一样。唯一的不同是模板的名字。 我们将在稍后解决这个冗余问题。

    from django.shortcuts import get_object_or_404, render
    
    def results(request, question_id):
        question = get_object_or_404(Question, pk=question_id)
        return render(request, 'polls/results.html', {'question': question})
    
  4. 创建一个 polls/results.html 模板

    <h1>{{ question.question_text }}</h1>
    <ul>
    {% for choice in question.choice_set.all %}
        <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
    {% endfor %}
    </ul>
    <a href="{% url 'polls:detail' question.id %}">Vote again?</a>
    
  5. 在浏览器中访问 /polls/1/ 然后为 Question 投票。可以看到一个投票结果页面,并且在每次投票之后都会更新。 如果提交时没有选择任何 Choice,应该看到错误信息。

7.2 使用通用视图:代码还是少点好

detail() 和 results() 视图都很精简 —— 并且,像上面提到的那样,存在冗余问题。用来显示一个投票列表的 index() 视图和它们类似。
▶ 这些视图反映基本的网络开发中的一个常见情况:根据 URL 中的参数从数据库中获取数据、载入模板文件然后返回渲染后的模板。 由于这种情况特别常见,Django 提供一种快捷方式,叫做 “通用视图” 系统。
▶ 通用视图将常见的模式抽象化,可以在编写应用时甚至不需要编写Python代码。
将投票应用转换成使用通用视图系统,这样可以删除许多代码。仅仅需要做以下几步来完成转换:
1)转换 URLconf。
2)删除一些旧的、不再需要的视图。
3)基于 Django 的通用视图引入新的视图。

为什么要重构代码?

一般来说,当编写一个 Django 应用时,应该先评估一下通用视图是否可以解决问题,应该在一开始使用它,而不是进行到一半时重构代码。本教程目前为止是有意将重点放在以“艰难的方式”编写视图,这是为将重点放在核心概念上。就像在使用计算器之前你需要掌握基础数学一样。

7.2.1 改良URLconf

打开 polls/urls.py 这个 URLconf 并将它修改成:

from django.urls import path
from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
7.2.2 改良视图

删除旧的 index, detail, 和 results 视图,并用 Django 的通用视图代替。打开 polls/views.py 文件,并将它修改成:

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """返回最后5个被发布的问题"""
        return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'

def vote(request, question_id):
    ... # 和之前一样,无需修改

▶ 这里使用两个通用视图: ListView 和 DetailView 。这两个视图分别抽象“显示一个对象列表”和“显示一个特定类型对象的详细信息页面”这两种概念。
▶ 每个通用视图需要知道它将作用于哪个模型。 这由 model 属性提供。
▶ DetailView 期望从 URL 中捕获名为 “pk” 的主键值,所以通用视图把question_id改成pk。
▶ 默认情况下,通用视图 DetailView 使用一个 < app name>/< model name>_detail.html 的模板。它使用 “polls/question_detail.html” 模板。template_name 属性是用来告诉 Django 使用一个指定的模板名字,而不是自动生成的默认名字。 为 results 列表视图指定了 template_name ,确保 results 视图和 detail 视图在渲染时具有不同的外观,即使它们在后台都是同一个 DetailView 。
▶ ListView 使用一个 < app name>/< model name>_list.html 的默认模板;使用 template_name 来告诉 ListView 使用创建的已经存在的 “polls/index.html” 模板。

在之前的操作中,提供模板文件时都带有一个包含 question 和 latest_question_list 变量的 context。对于DetailView , question 变量会自动提供—— 因为使用 Django 的模型(Question), Django能够为 context 变量决定一个合适的名字。
然而对于 ListView, 自动生成的 context 变量是question_list。为了覆盖这个行为,提供 context_object_name 属性,表示想使用latest_question_list。作为一种替换方案,可以改变模板来匹配新的 context 变量 ——这是一种更便捷的方法,告诉 Django 使用想使用的变量名。

最后,启动服务器,使用一下基于通用视图的新投票应用。

八、自动化测试

8.1 自动化测试是什么?

测试代码,是用来检查你的代码能否正常运行的程序。

测试在不同的层次中都存在。有些测试只关注某个很小的细节(某个模型的某个方法的返回值是否满足预期?),而另一些测试可能检查对某个软件的一系列操作(某一用户输入序列是否造成了预期的结果?)。

8.2 自动化测试的作用

1)测试将节约你的时间
2)测试不仅能发现错误,而且能预防错误
3)测试使你的代码更有吸引力
4)测试有利于团队协作

8.3 开始写第一个测试
  1. 首先得有个 Bug:polls 应用现在就有一个小 bug 需要被修复:要求是如果 Question 是在一天之内发布的, Question.was_published_recently() 方法将会返回 True ,然而现在这个方法在 Question 的 pub_date 字段比当前时间还晚时也会返回 True(这是个 Bug)。
    打开Terminal命令行,输入python manage.py shell,进入交互命令行。

    >>> import datetime
    >>> from django.utils import timezone
    >>> from polls.models import Question
    >>> # 创建一个获取未来30天问题的实例
    >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
    >>> # 是最近发布的吗
    >>> future_question.was_published_recently()
    True
    

    因为将来发生的是肯定不是最近发生的,所以代码明显是错误的。

  2. 创建一个测试来暴露这个 bug,在 shell 里做的测试也就是自动化测试应该做的工作。按照惯例,Django 应用的测试应该写在应用的 tests.py 文件里。测试系统会自动的在所有以 tests 开头的文件里寻找并执行测试代码。将下面的代码写入 polls/tests.py 文件内:

    import datetime
    from django.test import TestCase
    from django.utils import timezone
    from .models import Question
    
    class QuestionModelTests(TestCase):
    
        def test_was_published_recently_with_future_question(self):
            """
            was_published_recently() 对于 pub_date 在未来的问题返回 False。
            """
            time = timezone.now() + datetime.timedelta(days=30)
            future_question = Question(pub_date=time)
            self.assertIs(future_question.was_published_recently(), False)
    

    创建了一个 django.test.TestCase 的子类,并添加了一个方法,此方法创建一个 pub_date 时未来某天的 Question 实例。然后检查它的 was_published_recently() 方法的返回值——False。

  3. 运行测试,输入以下代码运行测试:python manage.py test polls
    运行结果:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    F
    ======================================================================
    FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
        self.assertIs(future_question.was_published_recently(), False)
    AssertionError: True is not False
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    FAILED (failures=1)
    Destroying test database for alias 'default'...
    

    以下是自动化测试的运行过程:
    ▶ python manage.py test polls 将会寻找 polls 应用里的测试代码
    ▶ 它找到了 django.test.TestCase 的一个子类
    ▶ 它创建一个特殊的数据库供测试使用
    ▶ 它在类中寻找测试方法——以 test 开头的方法。
    ▶ 在 test_was_published_recently_with_future_question 方法中,它创建了一个 pub_date 值为 30 天后的 Question 实例。
    ▶ 接着使用 assertls() 方法,发现 was_published_recently() 返回了 True,而我们期望它返回 False。
    测试系统通知我们哪些测试样例失败了,和造成测试失败的代码所在的行号。

  4. 修复这个 bug,当 pub_date 为未来某天时, Question.was_published_recently() 应该返回 False。修改 polls/models.py 里的方法,让它只在日期是过去式的时候才返回 True:

    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now
    
  5. 然后重新运行测试:

    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK
    Destroying test database for alias 'default'...
    

    发现 bug 后,编写了能够暴露这个 bug 的自动化测试。在修复 bug 之后,代码顺利的通过了测试。将来,应用可能会出现其他的问题,但是可以肯定的是,一定不会再次出现这个 bug,因为只要运行一遍测试,就会立刻收到警告。可以认为应用的这一小部分代码永远是安全的。

8.4 更全面的测试

已经搞定一小部分了,现在可以考虑全面的测试 was_published_recently() 这个方法以确定它的安全性,然后就可以把这个方法稳定下来了。事实上,在修复一个 bug 时不小心引入另一个 bug 会是非常令人尴尬的。

在上次写的 polls/tests.py 里再增加两个测试,来更全面的测试这个方法:

def test_was_published_recently_with_old_question(self):
    """
    对于pub_date早于 1 天的问题,was_published_recently() 返回 False。
    """
    time = timezone.now() - datetime.timedelta(days=1, seconds=1)
    old_question = Question(pub_date=time)
    self.assertIs(old_question.was_published_recently(), False)

def test_was_published_recently_with_recent_question(self):
    """
    was_published_recently() 对于 pub_date 在最后一天内的问题返回 True。
    """
    time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
    recent_question = Question(pub_date=time)
    self.assertIs(recent_question.was_published_recently(), True)

三个测试来确保 Question.was_published_recently() 方法对于过去,最近,和将来的三种情况都返回正确的值。

尽管 polls 现在是个小型的应用,但是无论它以后变得到多么复杂,无论他和其他代码如何交互,这样可以在一定程度上保证编写测试的方法将按照预期的方式运行。

8.5 测试视图

投票应用对所有问题都一视同仁:它将会发布所有的问题,也包括那些 pub_date 字段值是未来的问题。我们应该改善这一点。如果 pub_date 设置为未来某天,这应该被解释为这个问题将在所填写的时间点才被发布,而在之前是不可见的。
——为了修复上述 bug ,先编写测试,然后修改代码。
事实上,这是一个「测试驱动」开发模式的实例,但其实这两者的顺序不太重要。
在第一个测试中,关注代码的内部行为。通过模拟用户使用浏览器访问被测试的应用来检查代码行为是否符合预期。

8.5.1 Django 测试工具之 Client

Django 提供了一个供测试使用的 Client 来模拟用户和视图层代码的交互。能在 tests.py 甚至是 shell 中使用它。依照惯例从 shell 开始,首先要做一些在 tests.py 里不是必须的准备工作。

  1. 在 shell 中配置测试环境:python manage.py shell

    >>> from django.test.utils import setup_test_environment
    >>> setup_test_environment()
    

    setup_test_environment() 安装了一个模板渲染器,能够检查响应上的一些额外属性,如 response.context,否则将无法使用此功能。
    请注意,这个方法不会建立一个测试数据库,所以下面的内容将针对现有的数据库运行,输出结果可能略有不同,这取决于已经创建了哪些问题。
    如果在settings.py 中的 TIME_ZONE 不正确,可能会得到意外的结果。
    如果不记得之前的配置,请在继续之前检查。

  2. 导入 django.test.TestCase 类(在后续 tests.py 的实例中,将会使用 django.test.TestCase 类,这个类里包含了client 实例,所以不需要这一步):

    >>> from django.test import Client
    >>> # 创建一个供我们使用的客户端实例
    >>> client = Client()
    
  3. 最后,输入以下命令使用 client 。

    >>> # 从'/'得到回应
    >>> response = client.get('/')
    Not Found: /
    >>> # 我们应该期望从该地址收到 404; 如果您反而看到“无效的 HTTP_HOST 标头”错误和 400 响应,则您可能省略了前面描述的。
    >>> response.status_code
    404
    >>> # 另一方面,我们应该期望在 '/polls/' 找到一些东西,我们将使用 'reverse()' 而不是硬编码的URL
    >>> from django.urls import reverse
    >>> response = client.get(reverse('polls:index'))
    >>> response.status_code
    200
    >>> response.content
    b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
    >>> response.context['latest_question_list']
    <QuerySet [<Question: What's up?>]>
    
8.5.2 改善视图代码

投票列表会显示将来的投票( pub_date 值是未来的某天)。现在需要修复这个问题。

  1. 打开 polls/views.py ,看到基于 ListView 的视图类 IndexView。

    class IndexView(generic.ListView):
        template_name = 'polls/index.html'
        context_object_name = 'latest_question_list'
    
        def get_queryset(self):
            """返回最后五个发布的问题"""
            return Question.objects.order_by('-pub_date')[:5]
    

    改进 get_queryset() 方法,让它能通过将 Question 的 pub_data 属性与 timezone.now() 相比较来判断是否应该显示此Question。

  2. 首先需要一行 import 语句:from django.utils import timezone

  3. 把 get_queryset 方法改写成下面这样:

    def get_queryset(self):
        """
        返回最近发布的五个问题(不包括将来发布的问题)。
        """
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]
    

    Question.objects.filter(pub_date__lte=timezone.now()) 返回一个查询集,其中包含 pub_date 小于或等于 - 即早于或等于 - timezone.now 的问题。

8.5.3 测试新视图

启动服务器、在浏览器中载入站点、创建一些发布时间在过去和将来的 Questions ,然后检验只有已经发布的 Questions 会展示出来,如果不想每次修改可能与这相关的代码时都重复这样做,就基于以上 shell 中的内容,再编写一个测试。

将下面的代码添加到 polls/tests.py ,写一个公用的快捷函数用于创建投票问题,再为视图创建一个测试类:

from django.urls import reverse

# 快捷函数 create_question,它封装了创建投票的流程,减少了重复代码。
def create_question(question_text, days):
    """
    使用给定的“question_text”创建一个问题,并发布到现在的给定“days”偏移量(过去发布的问题为负数,尚未发布的问题为正数)。
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)


class QuestionIndexViewTests(TestCase):
	# test_no_questions 方法里没有创建任何投票,它检查返回的网页上有没有 "No polls are available." 这段消息和 latest_question_list 是否为空。
    def test_no_questions(self):
        """
        如果不存在任何问题,则会显示相应的消息。
        """
        response = self.client.get(reverse('polls:index'))
        
        # django.test.TestCase 类提供了一些额外的 assertion 方法,在这个例子中,我们使用了 assertContains() 和 assertQuerysetEqual() 。
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])
	
    def test_past_question(self):
        """
        带着 pub_date 过去的问题显示在索引页上。
        """
        question = create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            [question],
        )
	
	# 在 test_future_question 中,我们创建 pub_date 在未来某天的投票。数据库会在每次调用测试方法前被重置,所以第一个投票已经没了,所以主页中应该没有任何投票。
    def test_future_question(self):
        """
        带有pub_date 未来的问题不会显示在索引页面上。
        """
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_future_question_and_past_question(self):
        """
        即使过去和未来的问题都存在,也只会显示过去的问题。
        """
        question = create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            [question],
        )

    def test_two_past_questions(self):
        """
        问题索引页面可能会显示多个问题。
        """
        question1 = create_question(question_text="Past question 1.", days=-30)
        question2 = create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            [question2, question1],
        )

实际上,测试就是假装一些管理员的输入,然后通过用户端的表现是否符合预期来判断新加入的改变是否破坏了原有的系统状态。

8.5.4 测试DetailView

还有一个问题:就算在发布日期时未来的那些投票不会在目录页 index 里出现,但是如果用户知道或者猜到正确的 URL ,还是可以访问到它们。所以得在 DetailView 里增加一些约束,打开 polls/views.py :

class DetailView(generic.DetailView):
    ...
    def get_queryset(self):
        """
        排除任何尚未发布的问题。
        """
        return Question.objects.filter(pub_date__lte=timezone.now())

然后,增加一些测试来检验 pub_date 在过去的 Question 能够被显示出来,而 pub_date 在未来的则不可以:

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        对于未来问题的detail view 返回 404 not found
        """
        future_question = create_question(question_text='Future question.', days=5)
        url = reverse('polls:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        对于过去问题的detail view 返回 问题文本
        """
        past_question = create_question(question_text='Past Question.', days=-5)
        url = reverse('polls:detail', args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)
8.5.5 更多的测试思路

1)可以给 ResultsView 也增加一个类似的 get_queryset 方法,并且为它创建测试。
2)可以从各个方面改进投票应用,但是测试会一直伴随我们。比方说,在目录页上显示一个没有选项 Choices 的投票问题就没什么意义。可以检查并排除这样的投票题。测试可以创建一个没有选项的投票,然后检查它是否被显示在目录上。当然也要创建一个有选项的投票,然后确认它确实被显示了。
3)可以让管理员能在目录上看见未被发布的那些投票,但是普通用户看不到。不管怎么说,如果想要增加一个新功能,那么同时一定要为它编写测试。

如果对测试有个整体规划,那么它们就几乎不会变得混乱。下面有几条好的建议:

  1. 对于每个模型和视图都建立单独的 TestClass
  2. 每个测试方法只测试一个功能
  3. 给每个测试方法起个能描述其功能的名字

九、静态文件

已经建立了一个经过测试的网络投票应用程序,现在需要添加一个样式表和一个图像。
▶ 除了服务端生成的 HTML 以外,网络应用通常需要一些额外的文件——比如图片,脚本和样式表——来帮助渲染网络页面。在 Django 中,把这些文件统称为“静态文件”。
▶ django.contrib.staticfiles 存在的意义:将各个应用的静态文件(和一些指明的目录里的文件)统一收集起来,这样一来,在生产环境中,这些文件就会集中在一个便于分发的地方。

9.1 自定义APP的界面和风格
  1. 在 polls 目录下创建一个名为 static 的目录。

    Django 将在该目录下查找静态文件,这种方式和 Diango 在 polls/templates/ 目录下查找 template 的方式类似。
    Django 的 STATICFILES_FINDERS 设置包含了一系列的查找器,它们知道去哪里找到 static 文件。AppDirectoriesFinder 是默认查找器中的一个,它会在每个 INSTALLED_APPS 中指定的应用的子文件中寻找名称为 static 的特定文件夹,就像在 polls 中刚创建的那个一样。管理后台采用相同的目录结构管理它的静态文件。

  2. 在 static 文件夹中创建一个名为 polls 的文件夹,再在 polls 文件夹中创建一个名为 style.css 的文件。

    样式表路径应是 polls/static/polls/style.css。因为AppDirectoriesFinder 的存在,在
    Django 中以 polls/style.css 的形式引用此文件,类似引用模板路径的方式。

    静态文件命名空间
    虽然可以像管理模板文件一样,把 static 文件直接放入 polls/static (而不是创建另一个名为 polls 的子文件夹),不过这实际上是一个很蠢的做法。Django 只会使用第一个找到的静态文件。如果在其它应用中有一个相同名字的静态文件,Django 将无法区分它们。因此,需要指引 Django 选择正确的静态文件,而最好的方式就是把它们放入各自的 命名空间 。也就是把这些静态文件放入另一个与应用名相同的目录中。

  3. 将以下代码放入样式表(polls/static/polls/style.css):

    li a {
        color: green;
    }
    
  4. 在 polls/templates/polls/index.html 的文件头添加以下内容:

    {% load static %}
    
    <link rel="stylesheet" href="{% static 'polls/style.css' %}">
    

    {% static %} 模板标签会生成静态文件的绝对路径。

  5. 启动服务器(如果它正在运行中,重新启动一次):python manage.py runserver

  6. 重新载入 http://localhost:8000/polls/ ,会发现有问题的链接是绿色的 (这是 Django 自己的问题标注方式),这意味着追加的样式表起作用了。

9.2 添加一个背景图
  1. 在 polls/static/polls/ 目录中创建一个images子目录。 在此目录中,添加想要用作背景的任何图像文件。 出于本教程的目的,使用名为 background.png 的文件,其完整路径为 polls/static/polls/images/background.png

  2. 在样式表 (polls/static/polls/style.css) 中添加对图像的引用:

    body {
        background: white url("images/background.png") no-repeat;
    }
    
  3. 浏览器重载 http://localhost:8000/polls/,将在屏幕的左上角见到这张背景图。

十、自定义管理站点

10.1 自定义后台表单

通过 admin.site.register(Question) 注册 Question 模型,Django 能够构建一个默认的表单用于展示。通常来说,能自定义表单的外观和工作方式。可以在注册模型时将这些设置告诉 Django。通过重排列表单上的字段来看看它是怎么工作的。

用以下内容替换 admin.site.register(Question),打开 polls/admin.py :

from django.contrib import admin
from .models import Question

# 创建一个模型后台类,接着将其作为第二个参数传给 admin.site.register() ——在需要修改模型的后台管理选项时这么做。
class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text']

admin.site.register(Question, QuestionAdmin)

以上修改使得 “Publication date” 字段显示在 “Question” 字段之前。只有两个字段时显得没啥用,但对于拥有数十个字段的表单来说,为表单选择一个直观的排序方法就很🆗

对拥有数十个字段的表单,如果更期望将表单分为几个字段集,则打开 polls/admin.py :

rom django.contrib import admin
from .models import Question

class QuestionAdmin(admin.ModelAdmin):
	# fieldsets 元组中的第一个元素是字段集的标题。
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date']}),
    ]

admin.site.register(Question, QuestionAdmin)
10.2 添加关联的对象

现在有了投票的后台页。不过,一个 Question 有多个 Choice,但后台页却没有显示多个选项。有两个方法可以解决这个问题。

  1. 仿照向后台注册 Question 一样注册 Choice,打开 polls/admin.py :

    from django.contrib import admin
    from .models import Choice, Question
    # ...
    admin.site.register(Choice)
    

    在这个表单中,“Question” 字段是一个包含数据库中所有投票的选择框。Django 知道要将 ForeignKey 在后台中以选择框 < select> 的形式展示。此时,只有一个投票。如果要添加其他选项,注意“Question”旁边的“Add another question”链接。 每个与另一个具有 ForeignKey 关系的对象都可以方便获得它。 单击“Add another question”时,将看到一个带有“Add question”表单的弹出窗口。 如果在该窗口中添加一个问题并单击“Save”,Django会将问题保存到数据库中并动态地将其添加正在查看的“Add choice”表单中的选定选项。

    因此,这是一种很低效地添加“ Choice”的方法。

  2. 创建“vote”对象时直接添加好几个选项。移除调用 register() 注册 Choice 模型的代码。随后,打开 polls/admin.py 修改 Question 的注册代码:

    from django.contrib import admin
    from .models import Choice, Question
    
    # Choice 对象要在 Question 后台页面编辑。默认提供 3 个足够的选项字段。
    class ChoiceInline(admin.StackedInline):
        model = Choice
        extra = 3
        
    class QuestionAdmin(admin.ModelAdmin):
        fieldsets = [
            (None,               {'fields': ['question_text']}),
            ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
        ]
        inlines = [ChoiceInline]
    
    admin.site.register(Question, QuestionAdmin)
    

    不过,仍然有点小问题。它占据了大量的屏幕区域来显示所有关联的 Choice 对象的字段。对于这个问题,Django 提供了一种表格式的单行显示关联对象的方法。

    要使用它,只需按如下形式修改 ChoiceInline 申明:

    class ChoiceInline(admin.TabularInline):
        #...
    

    通过 TabularInline (替代 StackedInline ),关联对象以一种表格式的方式展示,显得更加紧凑。

10.3 自定义后台更改列表

对“更改列表”页面进行一些调整——改成一个能展示系统中所有投票的页面。
以下是它此时的外观:
在这里插入图片描述
默认情况下,Django 显示每个对象的 str() 返回的值。但有时如果需要显示单个字段,它会更有帮助。为此,使用 list_display 后台选项,它是一个包含要显示的字段名的元组,在更改列表页中以列的形式展示这个对。

打开 polls/admin.py,

class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ('question_text', 'pub_date')

另外,把 was_published_recently() 方法也加上:

class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ('question_text', 'pub_date', 'was_published_recently')

现在修改投票的列表页看起来像这样:
在这里插入图片描述

点击列标题来对这些行进行排序——除了 was_published_recently 这个列,因为没有实现排序方法。顺便看下这个列的标题 was_published_recently,默认就是方法名(用空格替换下划线),该列的每行都以字符串形式展示出处。

可以通过在该方法上(在 polls/models.py 中)使用 display() 装饰器来改进:

from django.contrib import admin

class Question(models.Model):
    # ...
    @admin.display(
        boolean=True,
        ordering='pub_date',
        description='Published recently?',
    )
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

再次编辑文件 polls/admin.py,优化 Question 变更页:过滤器,使用 list_filter。将以下代码添加至 QuestionAdmin:list_filter = ['pub_date']
添加了一个“过滤器”侧边栏,允许人们以 pub_date 字段来过滤列表:
在这里插入图片描述
展示的过滤器类型取决要过滤的字段的类型。因为 pub_date 是类 DateTimeField,Django 知道要提供哪个过滤器:“任意时间”,“今天”,“过去7天”,“这个月”和“今年”。

最后,再扩充些功能:search_fields = ['question_text']在列表的顶部增加一个搜索框。当输入待搜项时,Django 将搜索 question_text 字段。你可以使用任意多的字段——由于后台使用LIKE 来查询数据,将待搜索的字段数限制为一个不会出问题大小,会便于数据库进行查询操作。

现在是修改列表页增加分页功能的好时机。默认每页显示 100 项。变更页分页, 搜索框, 过滤器, 日期层次结构, 和 列标题排序均以期望的方式合作运行。

10.4 自定义后台的界面和风格

在每个后台页顶部显示“Django 管理员”显得很滑稽。这只是一串占位文本。可以通过 Django 的模板系统来修改。Django 的后台由自己驱动,且它的交互接口采用 Django 自己的模板系统。

10.4.1 自定义工程的模板

在工程目录(指包含 manage.py 的那个文件夹)内创建一个名为 templates 的目录。模板可放在系统中任何 Django能找到的位置。(谁启动了 Django,Django 就以他的用户身份运行。)把模板放在工程内会带来很大便利,推荐使用。

  1. 打开设置文件(mysite/settings.py),在 TEMPLATES 设置中添加 DIRS 选项:

    TEMPLATES = [
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            'DIRS': [BASE_DIR / 'templates'],
            'APP_DIRS': True,
            'OPTIONS': {
                'context_processors': [
                    'django.template.context_processors.debug',
                    'django.template.context_processors.request',
                    'django.contrib.auth.context_processors.auth',
                    'django.contrib.messages.context_processors.messages',
                ],
            },
        },
    ]
    
  2. DIRS 是一个包含多个系统目录的文件列表,用于在载入 Django 模板时使用,是一个待搜索路径。

    组织模板
    就像静态文件一样,我们 可以 把所有的模板文件放在一个大模板目录内,这样它也能工作的很好。但是,属于特定应用的模板文件最好放在应用所属的模板目录(例如 polls/templates),而不是工程的模板目录(templates)。

  3. 在 templates 目录内创建名为 admin 的目录,随后,将存放 Django 默认模板的目录(django/contrib/admin/templates)内的模板文件 admin/base_site.html 复制到这个目录内。

    Django 的源文件在哪里? 如果不知道 Django 源码在你系统的哪个位置,运行以下命令: python -c "import django; print(django.__path__)"
    用你网页站点的名字编辑替换文件内的 {{ site_header|default:_(‘Django administration’) }} (包含大括号)。完成后,你应该看到如下代码:

    {% block branding %}
    <h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
    {% endblock %}
    

    用这个方法来教你复写模板。在一个实际工程中,你可能更期望使用django.contrib.admin.AdminSite.site_header 来进行简单的定制。

    这个模板文件包含很多类似 {% block branding %} 和 {{ title }} 的文本。 {% 和 {{ 标签是Django 模板语言的一部分。当 Django 渲染 admin/base_site.html时,这个模板语言会被求值,生成最终的网页,就像我们在 教程第 3 部分 所学的一样。 注意,所有的 Django 默认后台模板均可被复写。若要复写模板,像你修改 base_site.html一样修改其它文件——先将其从默认目录中拷贝到你的自定义目录,再做修改。

10.4.2 自定义APP的模板

DIRS 默认是空的,Django 是怎么找到默认的后台模板的?因为 APP_DIRS 被置为 True,Django会自动在每个应用包内递归查找 templates/ 子目录(不要忘了 django.contrib.admin 也是一个应用)。

投票应用不是非常复杂,所以无需自定义后台模板。不过,如果它变的更加复杂,需要修改 Django 的标准后台模板功能时,修改 应用的模板会比 工程 的更加明智。这样,在其它工程包含这个投票应用时,可以确保它总是能找到需要的自定义模板文件。

10.5 自定义后台主页

在类似的说明中,你可能想要自定义 Django 后台索引页的外观。

默认情况下,它展示了所有配置在 INSTALLED_APPS 中,已通过后台应用注册,按拼音排序的应用。你可能想对这个页面的布局做重大的修改。毕竟,索引页是后台的重要页面,它应该便于使用。

需要自定义的模板是 admin/index.html。(像修改 admin/base_site.html 那样修改此文件——从默认目录中拷贝此文件至自定义模板目录)。打开此文件,你将看到它使用了一个叫做 app_list 的模板变量。这个变量包含了每个安装的 Django 应用。你可以用任何你期望的硬编码链接(链接至特定对象的管理页)替代使用这个变量。

十一、提高教程

如何创建可复用应用

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值