Django:Web 框架从入门到应用

Django 是基于 MVC 模式,由 Python 写成的开源 Web 应用框架。在 Django 中,控制器接受用户输入的部分由框架自行处理,而 Django 里更关注的是模型 ( Model )、模板 ( Template ) 和视图 ( Views ),为此也称其为 MTV 模式的 Web 框架。

事实上,基于 Python 的 Web 框架不仅这一家,如 flask、tornado、web2py 等。对于任何一款框架都有它自身的亮点和缺陷 $^{[2, 3]}$,综合需求、性能要求等诸多因素考量,选择合适的框架即可。比如,我们要开发一款数据库驱动的内容发布与管理系统,借助 Django 的中间件 ORM 设计优点,使得在操作业务对象时,不需要和复杂的 SQL 语句打交道,只要像平时一样操作对象即可,以高效率完成轻量级后端系统的开发工作。

教学资源

快速上手

安装与配置

  • 安装:命令行模式安装 ( Mac / Linux 用户注意管理员权限 )

    1
    2
    pip install django
    pip install django == x.xx.xx
  • 配置:配置 Django 项目并初次启动它。

    • 创建项目:可通过命令模式启动项目 ( 多用于部署环境 ),也可通过 PyCharm 启动、运行项目。

      • 命令模式:django-admin startproject mysite
      • PyCharm:通过 PyCharm 一步到位,即 新建工程 > Django > ( 建立单独的 Venv ) > mysite

        Virtualenv:为一个应用创建一套“隔离”的 Python 运行环境,具体配置方法可参考 [1]。

    • 项目目录结构说明

      • mysite:同名目录,它是一个纯 Python 包。它的名字就是当你引用它内部任何东西时需要用到的 Python 包名,比如 mysite.urls。
      • __init__.py:空文件,标识这个目录应识别为 Python 包。
      • settings.py:Django 的项目配置文件。
      • urls.py:Django 项目的 URL 声明。
      • wsgi.py:Web 服务网关接口 ( Socket )。
      • manage.py:对网络所有管理是通过其来实现的。

        1
        2
        3
        4
        5
        6
        7
        mysite
        ├─── mysite
        │ ├── init__.py
        │ ├── settings.py
        │ ├── urls.py
        │ ├── wsgi.gy
        └─── manage.py
  • 使用:以命令模式启动本地服务器为例,当然可以使用 Pycharm 一键运行。

    1
    python manage.py runserver 127.0.0.1:8000

请求与响应

返回内容至页面

返回内容 ( Html 元素或对象 ) 至页面,代码应包含在 urls.py 文件中。

  • 下述是返回字符串或 Html 元素的示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from django.shortcuts import HttpResponse
    '''
    arguments:
    @param request 放置用户请求相关的所有信息
    @return 响应与返回处理结果
    '''
    def info(request):
    # 1. 可返回字符串
    return HttpResponse("Hello World!")
    # 2. 可返回 Html 元素 (对象)
    # return HttpResponse("<input type='text' />")

    # 访问 Site 根目录
    urlpatterns = [ url('', info), ]

返回独立 Html 页面

返回独立 Html 页面,且尝试把数据返回到页面中。

  • Html 页面放置 tempates 目录下;
  • settings.py 中配置模板的路径 ( 告诉程序网页模板在哪个目录下 );

    1
    2
    3
    TEMPLATES = [
    'DIRS': [os.path.join(BASE_DIR, 'templates')]
    ]
  • urs.py 中加入调用代码,绑定请求地址与处理函数;

    1
    2
    3
    def info(request):
    # render 能抓取页面全部信息 ( 它也调用了 HttpResponse )
    return render(request, 'info.html')
  • 若要引用资源目录,如存放 images、css 等,则需要在 settings.py 中加入声明语句。

    引用资源时,需要加入 static。例如 <link rel="stylesheet" href="static/style.css">

    1
    2
    3
    4
    5
    STATIC_URL = '/static/'
    STATICFILES_DIRS = (
    # '文件名' 是自由命名的,这里取 'static' 是为了统一命名
    os.path.join(BASE_DIR, 'static'),
    )

简单登录功能

本实例主要展示的是,在 Django 框架下 Web 前后端的交互过程。

  • 首先,我们在 urls.py 中配置路由关系,并绑定路由触发的函数以实现功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    from django.shortcuts import HttpResponse, render, redirect
    from django.urls import path

    # 首次加载页面调用的函数
    def login(request):
    # GET 可通过请求的链接地址传参,如 url?page=1
    if ('GET' == request.method):
    ## render() 是抓取页面全部信息 ( 它也调用了 HttpResponse )
    return render(request, 'login.html')

    # 登录成功后把数据回传到目标页面
    def index(request):
    user = request.POST
    return render(request, 'index.html', {
    'username': str(user.get('username')),
    'password': {
    'origin': user.get('password'),
    'encode': base64.b64encode( (user.get('password') + user.get('password')).encode('utf-8') )
    }}
    )

    # path(相对地址, 调用函数),如请求地址为根目录,故这里填写 ''
    urlpatterns = [
    path('', login),
    path('index', index),
    ]
  • 最后附上相关联的 login.htmlindex.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <!-- login.html -->
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Information Page</title>
    <link rel="stylesheet" href="static/css/style.css">
    </head>
    <body>
    <h2>Hello World!</h1>
    <h2>Welcome to use the exhibation page.</h2>
    <form method="POST" action="index">
    <input name="username" type="text" />
    <input name="password" type="password" />
    <input value="login" type="submit" />
    </form>
    </body>
    </html>

    <!-- index.html -->
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Home Index</title>
    <link rel="stylesheet" href="static/css/style.css">
    </head>
    <body>
    <!-- 1. 模板中接受函数体返回的属性 -->
    <!-- {{}} 特殊占位符: Django render() 会自动解析它 -->
    <h2>username</h2> {{ username }}
    <h2>password</h2> {{ password.origin }} => {{ password.encode }}

    <p><!-- 换行 --></p>

    <!-- 2. 模板中调用对象的方法和属性,例如循环体 -->
    <table border="1">
    <tr>
    <td>Username</td><td>{{ username }}</td>
    </tr>
    {% for key, value in password.items %}
    <tr>
    <td>Password:{{ key }}</td><td>{{ value }}</td>
    </tr>
    {% endfor %}
    </table>
    </body>
    </html>

增删改查系统

本实例主要展示的是 Python 与数据库的交互过程。

建立数据表

数据关系:这里以学生 ( Student )、任教老师 ( Teacher ) 和课程 ( Course ) 三个实体为例,构建数据表。

  • ER 图,如图 4-1 所示:
    • 学生可以选修多门课程,一门课程可以多个学生参与,即多对多关系。
    • 老师只能任教一门课程,但是一门课程有多个老师开课,即一对多关系。

django_1-1

图 1-1 演示数据库的数据关系
  • 关系模式:
    • 学生实体(学生序号, 学生姓名) == tstudent(_sid, name)
    • 课程实体(课程序号, 课堂名称) == tcourse(_cid, name)
    • 老师实体(老师序号, 老师姓名) == tteacher(_tid, name, cid)
    • 选课关系(学生序号, 课程序号, 成绩) == Student2Course(sid, cid, score)

前 / 后端交互原理

  • Web 程序的前后端交互原理如图 4-2 所示。

django_1-2

图 1-2 Web 程序的前后端交互原理

原生代码操作数据

  • Python 的 MySQL 驱动有 MySQLdbPyMySQL 以及 MySQLClient
  • 在 Python 2.7 版本,主要是用 MySQLdb,而 Python 3.x 版本多数使用 PyMySQL 以及 MySQLClient。两者操作风格类似,本文则以 PyMySQL 展开探讨。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 封装连接数据库的信息
    db_infos = {
    'host': "IP 地址",
    'port': 3306,
    'user': "数据库账户",
    'password': "数据库密码",
    'db': "数据库名称",
    'charset': "utf8",
    'cursorclass': pymysql.cursors.DictCursor
    }

    import pymysql
    db = pymysql.connect(db_infos)

    SQL = "SELECT * FROM t_student"

    try:
    with db.cursor() as cursor:
    cursor.excute(SQL) # 执行 SQL 语句
    db.commit() # 提交修改数据请求
    except:
    db.rollback() # 回滚
    finally:
    db.close() # 关闭数据库连接

Ajax 方式交互数据

  • 借助 Ajax,实现数据的本地刷新,而不需要重新加载、渲染网页。

    1
    2
    3
    4
    5
    6
    7
    8
    $.ajax({
    url: '提交地址',
    type: 'POST' // POST / GET
    data: {'key_1': 'value_1', ..., 'key_n': 'value_n' }
    success: function(data){
    // 当前服务端处理数据,自动执行回调函数
    }
    })

框架正文

本章节的内容参考 Django 官方文档 v2.1 整理所得,即把模型、模板和视图的概念更加细化,通过一个投票应用的实例以讲述如何搭建一个 MTV模式 的 Web 框架,如图 3-1 所示。

django_2-1

图 2-1 MTV 模式的 Web 框架

数据库配置

  • 编辑 mysite/settings.py 文件前,先设置 TIME_ZONE 为你自己时区,可参考 Wikipedia 的 List of time zones

    新建立的项目,默认时区为 UTC

  • 打开 mysite/settings.py,通常这个配置文件使用 SQLite 作为默认数据库。

    • ENGINE:选值 django.db.backends.sqlite3
    • NAME:数据库的名称。若使用 SQLite,数据库将是你电脑上的一个文件,NAME 应该是此文件的绝对路径 + 文件名,默认值 os.path.join(BASE_DIR, 'db.sqlite3') 将会把数据库文件储存在项目的根目录。

      1
      2
      3
      4
      5
      6
      DATABASES = {
      'default': {
      'ENGINE': 'django.db.backends.sqlite3',
      'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
      }
      }
    • 若使用了 SQLite 以外的数据库,请确认在使用前已经 创建了数据库。连接到其他数据库时 ( MySQL,Oracle 或 PostgreSQL ),参考 ENGINE 的设置来连接其他数据库。例如连接 MySQL 的配置如下所示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      DATABASES = {
      'default': {
      'ENGINE': 'django.db.backends.mysql',
      'NAME': 'mydatabase',
      'USER': 'mydatabaseuser',
      'PASSWORD': 'mypassword',
      'HOST': '127.0.0.1',
      'PORT': '3306',
      }
      }
  • 此外,关注一下文件头部的 INSTALLED_APPS 设置项。这里包括了项目中启用的所有 Django 应用。应用能在多个项目中使用,也可以打包并且发布应用,让别人使用它们。通常 INSTALLED_APPS 默认包括了以下 Django 的自带应用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    INSTALLED_APPS = [
    # 因为 ApplicationConfig 类写在文件 polls/apps.py 中,
    # 所以它的点式路径是 'polls.apps.ApplicationConfig'
    'polls.apps.ApplicationConfig', # 激活模型
    'django.contrib.admin', # 管理员站点
    'django.contrib.auth', # 认证授权系统
    'django.contrib.contenttypes', # 内容类型框架
    'django.contrib.sessions', # 会话框架
    'django.contrib.messages', # 消息框架
    'django.contrib.staticfiles', # 管理静态文件的框架
    ]
  • 默认开启的某些应用需要至少一个数据表,故在使用他们前需要在数据库中创建一些表。请执行命令:

    1
    2
    3
    4
    5
    # MacOS / Linux
    python manage.py migrate

    # Windows ( 下述代码同理,基本上 "py" 对应于 "python" )
    py manage.py migrate

模型和站点管理

创建模型

  • 定义模型 ( Model ),即数据库结构设计和附加的其它元数据。在 Django 中,你只需要定义数据模型,其中的实现代码不用理会,它们会自动从模型生成。

    模型是真实数据的简单明确的描述,它包含了储存的数据所必要的字段和行为。

  • 例如,在本案例中 ( 投票应用 ),需要创建两个模型:问题 Question 和选项 Choice。

    • Question 模型:包括问题描述和发布时间。
    • Choice 模型:包括选项描述和当前得票数。每个选项属于一个问题 ( 一对一关系 )。
  • 这些概念可通过 Python 类来描述。按照下面的例子来编辑 polls/models.py 文件:

    • 每个模型被表示为 django.db.models.Model 类的子类。每个模型有一些类变量,它们都表示模型里的一个数据库字段。
    • 每个字段都是 Field 类的实例,这将告诉 Django 每个字段要处理的数据类型。比如,字符字段被表示为 CharField,日期时间字段被表示为 DateTimeField
    • 定义某些 Field 类实例需要参数,例如 CharField 需要一个 max_length 参数。

      这个参数的用处不止于用来定义数据库结构,也用于验证数据。

    • Django 支持所有常用的数据库关系:一对一、一对多和多对多,我们使用外键 ForeignKey 定义了一个关系。例如,每个 Choice 对象都关联到一个 Question 对象。

    • 数据表最重要的 主键 会被自动创建,当然也可以自定义。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      from django.db import models

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

      # Question.objects.all() 返回信息对我们用处不大,如下所示:
      # <QuerySet [<Question: Question object (1)>]>
      # 可尝试通过 __str__() 方法返回一些字段信息
      def __str__(self):
      return self.question_text

      class Choice(models.Model):
      question = models.ForeignKey(Question, on_delete=models.CASCADE)
      choice_text = models.CharField(max_length=200)
      votes = models.IntegerField(default=0)
      def __str__(self):
      return self.choice_text

激活模型

  • 从上述用于创建模型的代码可知,Django 可实现:

    • 为这个应用创建数据库 schema ( 生成 CREATE TABLE 语句 )。
    • 创建与 Question 和 Choice 对象与数据库进行交互的 API ( Python 版本 )。
  • 但是首先得把 polls 应用安装到我们的项目里。具体地, 在文件 mysite/settings.pyINSTALLED_APPS 子项添加点式路径:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    INSTALLED_APPS = [
    # 因为 ApplicationConfig 类写在文件 polls/apps.py 中,
    # 所以它的点式路径是 'polls.apps.ApplicationConfig'
    'polls.apps.ApplicationConfig', # 激活模型
    'django.contrib.admin', # 管理员站点
    'django.contrib.auth', # 认证授权系统
    'django.contrib.contenttypes', # 内容类型框架
    'django.contrib.sessions', # 会话框架
    'django.contrib.messages', # 消息框架
    'django.contrib.staticfiles', # 管理静态文件的框架
    ]
  • 现在你的 Django 项目会包含 polls 应用。接着通过运行 makemigrations 命令,Django 会检测你对模型文件的修改 ( 在这种情况下刚创建的可理解为最新修改的 ),并且把修改的部分储存为一次 迁移

    迁移:Django 对于模型定义,也就是你的数据库结构的变化的储存形式。它们其实也只是一些你磁盘上的文件。

    1
    python manage.py makemigrations polls
  • Django 有一个自动执行 数据库迁移 并同步管理你的数据库结构的命令:

    1
    python manage.py migrate
  • 当然,你是否会好奇,迁移是怎样的过程,迁移命令会执行哪些 SQL 语句?那么,sqlmigrate 命令接收一个迁移的名称,然后返回对应的 SQL。

    1
    2
    3
    # sqlmigrate 命令
    # 并没有真正在数据库中的执行迁移,它只是把命令输出到屏幕上
    python manage.py sqlmigrate polls 0001
  • 输入以上命令,你将看到如下结果 ( 格式化输出 SQL ):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    -- 输出示例使用的是 PostgreSQL / MySQL --
    BEGIN;
    -- Create model Choice
    CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL
    );
    -- Create model Question
    CREATE TABLE "polls_question" (
    "id" serial NOT NULL PRIMARY KEY,
    "question_text" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
    );
    -- 省略剩余语句,具体可自行测试 --
    COMMIT;
  • 从格式化的 SQL 语句可注意到:

    • 数据库的表名是由应用名 ( polls ) 和模型名的小写形式 ( question 和 choice ) 连接而来。

    • 主键 ( id ) 会被自动创建,当然你也可以自定义。

    • 默认 Django 会在外键字段名后追加字符串 “_id” ,同样也可以自定义。

    • 生成的 SQL 语句是为你所用的数据库定制的,所以那些和数据库有关的字段类型,比如 auto_increment ( MySQL )、 serial ( PostgreSQL ) 和 integer primary key autoincrement ( SQLite ),Django 会帮你自动处理。那些和引号相关的事情,比如使用单引号还是双引号,也一样会被自动处理。

  • 总结:迁移是非常强大的功能,它能让你在开发过程中持续的改变数据库结构而不需要重新删除和创建表,即它专注于使数据库平滑升级而不会丢失数据。现在改变模型只需要记住这三步

    • 编辑 models.py 文件,改变模型。

    • 运行 python manage.py makemigrations 为模型的改变 生成 迁移文件。

    • 运行 python manage.py migrate应用 数据库迁移。

数据操作

  • 当完成 创建模型 ( 定义数据实体和数据关系 ) 与 激活模型 ( 模型驱动自动生成 SQL 代码 ) 的工作,即表明数据表已建立起来,紧接着便可操作数据库了。
  • 关于操作数据库的 Python API 所有细节可在 Database API For Python 参考文档中找到。
创建对象
  • 假设模型存在于文件中 mysite/polls/models.py:

    1
    2
    3
    4
    5
    from polls.models import Choice, Question
    # 使用模型类的关键字参数对其实例化,再调用 save() 以将其保存到数据库中
    q = Question(question_text="What's new?", pub_date=timezone.now())
    # 创建和保存对象则使用 create() 方法
    q.save()
更新对象
  • UPDATE 在幕后执行 SQL 语句:

    1
    2
    3
    # 保存对象的更改
    q.question = "What do you think about?"
    q.save()
  • 保存 ForeignKey 字段:更新 ForeignKey 字段的工作方式与保存普通字段的方式完全相同,只需将正确类型的对象分配给相关字段即可。

    1
    2
    3
    4
    5
    # 一般情况 pk ( Primary Key ) 和 id 是一样的,只有 id 不是主键时才不一样
    choice = Choice.objects.get(pk=1)
    question = Question.objects.get('What do you think about?')
    choice.question = question
    choice.save()
  • 更新ManyToManyField 工作的方式略有不同 :使用 add() 字段上的方法向关系添加记录。
检索对象
  • 使用 all() 返回所有对象:例如,返回 Question 数据库中所有对象。

    1
    all_question = Question.objects.all()
  • 使用过滤器检索特定对象:若我们仅需要选择整个对象集的子集,则需要向 QuerySet 添加过滤条件。两种最常见的改进方法:

    • filter(**kwargs):返回 QuerySet 包含与给定查找参数匹配的新对象。
    • exclude(**kwargs):返回 QuerySet 包含与给定查找参数不匹配的新对象。

      1
      2
      # 例如:获取 2018 年间所有问题记录
      Question.objects.filter(pub_date__year=2017)
  • 链接过滤器:QuerySet 检索结果本身也是 QuerySet 对象,故可使用多个过滤器。作用与 多条件查询 类似效果。

    1
    2
    3
    4
    5
    6
    7
    8
    # 例如:获取以“What”开头,在 2018 年 1 月 1 日至当天前的所有记录
    Question.objects.filter(
    headline__startswith='What'
    ).exclude(
    pub_date__gte=datetime.date.today()
    ).filter(
    pub_date__gte=datetime.date(2018, 1, 1)
    )
  • 使用 get() 检索单个对象:

    1
    2
    one_question = Question.objects.get(pk=1)
    # Question 没有主键为 1 的对象,Django 将引发异常 Entry.DoesNotExist。
  • 限制 QuerySet 返回集合的大小:使用 Python 的数组切片语法将限制 QuerySet 为一定数量的结果。这相当于 SQL 中 LIMITOFFSET 子句。

    • 注意 1:Entry.objects.all()[-1] 不支持负索引。
    • 注意 2:Entry.objects.all()[:10:2] 不支持使用步进 ( Step ) 取值。

      1
      2
      3
      4
      # 例如:返回前5个对象():LIMIT 5
      Question.objects.all()[:5]
      # 例如:返回第6到第10个对象:OFFSET 5 LIMIT 5
      Question.objects.all()[5:10]

站点管理

开篇引言
  • 相信你也有过同样的经历,例如为你的员工或客户生成一个用户添加、修改和删除内容的管理后台,即简单的增删改查操作 ( CRUD ) ,但它却是一项缺乏创造性和乏味的工作。因此,Django 全自动地根据模型创建界面化的管理后台。
  • 管理界面不是为了网站的访问者,而是为管理者准备的。需要客制化的后台管理界面还需自行实现。
创建管理员账号
  • 首先,我们得创建一个能登录管理页面的用户。请运行下面的命令:

    1
    2
    3
    4
    5
    6
    python manage.py createsuperuser
    # Username: kofe
    # Email address: kofe@example.com
    # Password: **********
    # Password (again): *********
    # Superuser created successfully.
启动开发服务器
  • Django 的管理界面默认就是启用的。让我们启动开发服务器。当然,你可以通过 PyCharm 启动服务器,也可以通过命令启动:

    1
    python manage.py runserver
  • 打开浏览器即可访问:http://127.0.0.1:8000/admin/

管理页添加应用
  • 在索引页面中,我们并没有看到应用,如本例中的投票应用 polls。所以我们得告诉管理页面,问题 Question 对象需要被管理。打开 polls/admin.py 文件,把它编辑成下面这样:

    1
    2
    3
    from django.contrib import admin
    from .models import Question
    admin.site.register(Question)

模板和视图

开篇引言

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

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

  • 视图可以从数据库里读取记录,可使用一个模板引擎 ( Django 自带或者其他第三方的 ),生成一个 PDF 文件、输出一个 XML、创建一个 ZIP 文件等,你可以使用任何你想用的 Python 库,实现你想做的事。

  • Django 自带的 Database API 很方便,与试图结合使用即可实现数据的基本交互操作。

    1
    2
    3
    4
    5
    6
    7
    8
    # 在 index() 函数里插入了一些新内容
    # 让它能展示数据库里以发布日期排序的最近5个投票问题
    from django.http import HttpResponse
    from .models import Question
    def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[0:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

模板系统

  • 这里有个问题:页面的设计写死在视图函数的代码里的。如果你想改变页面的样子,你需要编辑 Python 代码。我们是否能将此过程相互分离,即 视图负责处理、组装数据模板则负责样式

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

      模板命名空间:虽然可将模板文件直接放在 mysite/templates 目录下,但若有一个模板文件正好和另一个应用中的某个模板文件重名,则 Django 没有办法区分它们,从而选择第一个匹配的模板文件,造成不能准确匹配的状况。

      帮助 Django 正确选择模板,最简单的方法是把他们放入各自的 命名空间 中,即把这些模板放入一个和 自身应用重名 的子文件夹里,如本例中的 polls

小试牛刀

  • 我们将下面的代码输入到刚刚创建的模板文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- mysite/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 %}
  • 然后,让我们更新一下 polls/views.py 里的 index 视图来使用模板:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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))
  • 载入模板,填充上下文,再返回由它生成的 HttpResponse 对象,这里引入一个便捷函数 render() 函数。它已经把此过程封装一起,调用即可使用。

    render() 函数的第一个参数是 request 对象,第二个参数是模板名,第三个参数是字典。它返回给定上下文呈现的给定模板的 HttpResponse 对象。

    1
    2
    3
    4
    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)
  • 用你的浏览器访问 http://127.0.0.1:8000/polls/,你将会看见一个无序列表。

使用模板系统

使用模板系统的过程,更像是前端与后端信息交互的过程,即前端访问请求地址获取数据的过程。

  • 模板系统统一使用 点符号 来访问变量的属性。在示例 {{ 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

  • 硬编码:硬编码和强耦合的链接,对于一个包含很多应用的项目来说,修改起来是十分困难的。

    1
    2
    3
    <li><a href="/polls/{{ question.id }}/">
    {{ question.question_text }}
    </a></li>
  • 软编码:然而,因为你在 polls.urls 的 url() 函数中通过 name 参数为 URL 定义了名字,你可以使用 {% url %} 标签代替它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!--
    | 具有名字 'detail' 的 URL 在 polls/url.py 中定义为:
    | path('<int:question_id>/', views.detail, name='detail')
    -->
    <li><a href="{% url 'detail' question.id %}">
    {{ question.question_text }}
    </a></li>

    <!--
    | 若你想改变投票详情视图的 URL,比如 polls/specifics/12/
    | 不用在模板里修改任何东西 (包括模板),只在 polls/urls.py 稍微修改就行
    | path('specifics/<int:question_id>/', views.detail, name='detail')
    -->

为 URL 名称添加命名空间

  • 在一个真实的 Django 项目中,可能会有多个应用,Django 如何分辨重名的 URL 呢?具体情况则是,{% url %} 标签到底对应哪一个应用的 URL 呢?
  • 在根 URLconf 中添加命名空间。在 polls/urls.py 文件中稍作修改,加上 app_name 设置命名空间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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'),
    ]
  • 修改为指向具有命名空间的详细视图:

    1
    2
    3
    <li><a href="{% url 'polls:detail' question.id %}">
    {{ question.question_text }}
    </a></li>

表单和通用视图

表单

在此小节中,通过表单接收数据,再通过 Django 视图来处理提交的数据。此过程,更像是前端打包数据通过 GET/POST 请求,把数据传送到后端,交由后端视图处理数据。

  • 编写一个简单的表单:让我们更新一下在上一个教程中编写的投票详细页面的模板 polls/detail.html,让它包含一个 HTML <form> 元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- mystie/templates/polls/detail.html -->
    <h1>{{ question.question_text }}</h1>
    {% if error_message %}
    <p><strong>{{ error_message }}</strong></p>
    {% endif %}
    <form action="{% url 'polls:vote' question.id %}" method="post">
    <!--
    | 跨站点请求伪造保护:
    | 在 Django 中,所有针对内部 URL 的 POST 表单,
    | 都应该使用 {% csrf_token %} 模板标签。
    -->
    {% csrf_token %}
    {% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <!-- 指示 for 标签已经循环多少次 -->
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
    <input type="submit" value="Vote">
    </form>

    跨站点请求伪造保护:当恶意网站包含链接,表单按钮或某些旨在在您的网站上执行某些操作的JavaScript时,会发生此类攻击,使用登录用户访问其浏览器中的恶意网站的凭据。一种相关类型的攻击,“登录CSRF”,攻击网站欺骗用户的浏览器以其他人的凭据登录网站也受到保护。

  • 我们为投票应用创建了一个 URLconf ,即新增一行 path()

    1
    2
    # polls/urls.py
    path('<int:question_id>/vote/', views.vote, name='vote')
  • 创建一个 vote() 函数,来处理相关的数据请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 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):
    # Redisplay the question voting form.
    return render(request, 'polls/detail.html', {
    'question': question,
    'error_message': "You didn't select a choice.",
    })
    else:
    selected_choice.votes += 1
    selected_choice.save()
    # Always return an HttpResponseRedirect after successfully dealing
    # with POST data. This prevents data from being posted twice if a
    # user hits the Back button.
    return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
    • 代码返回一个 HttpResponseRedirect 而不是常用的 HttpResponse;
    • HttpResponseRedirect 只接收一个参数:用户将要被重定向的 URL;
    • 在 HttpResponseRedirect 的构造函数中使用 reverse() 函数。这个函数避免了我们在视图函数中硬编码 URL。

      reverse() 调用将返回一个这样的字符串 /polls/3/results/
      其中 3 是 question.id 的值。重定向的 URL 将调用 ‘results’ 视图来显示最终的页面。

  • 当对 Question 进行投票后,vote() 视图将请求重定向到 Question 的结果界面。让我们来编写这个视图 ( 这和上一章节中的 detail() 视图几乎一模一样,唯一的不同是模板的名字。 我们将在稍后解决这个冗余问题 ):

    1
    2
    3
    4
    5
    6
    # polls/views.py
    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})
  • 再创建一个 polls/results.html 模板:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- polls/templates/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>

通用视图

猜想:通用视图是否是通用模板的思想,即使用统一的界面展示数据?

  • detail() 和 results() 视图都很简单。并且,像上面提到的那样,存在冗余问题。
  • 这些视图反映基本的 Web 开发中的一个常见情况:根据 URL 中的参数从数据库中获取数据、载入模板文件然后返回渲染后的模板。 由于这种情况特别常见,Django 提供一种快捷方式,叫做“通用视图”系统。
  • 通用视图将常见的模式抽象化,可以使你在编写应用时甚至不需要编写 Python 代码。

    一般来说,当编写一个 Django 应用时,你应该先评估一下通用视图是否可以解决你的问题,你应该在一开始使用它,而不是进行到一半时重构代码。

  • 让我们将我们的投票应用转换成使用通用视图系统,这样我们可以删除许多我们的代码。我们仅仅需要做以下几步来完成转换,我们将:

    • 转换 URLconf。
    • 删除一些旧的、不再需要的视图。
    • 基于 Django 的通用视图引入新的视图。
改良 URLconf
  • 首先,打开 polls/urls.py 这个 URLconf 并将它修改成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from django.urls import path
    from . import views

    app_name = 'polls'
    urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    # 路径字符串中匹配模式的名称已经由 <question_id> 改为 <pk>
    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'),
    ]
改良视图
  • 下一步,我们将删除旧的 index, detail, 和 results 视图,并用 Django 的通用视图代替。打开 polls/views.py 文件,并将它修改成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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):
    # Return the last five published questions.
    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):
    # Same as above, no changes needed.
  • 上述代码详细解释:

    • 默认情况下,通用视图 DetailView 使用一个叫做 <app name>/<model name>_detail.html 的模板。在我们的例子中,它将使用 polls/question_detail.html 模板。
    • template_name 属性是用来告诉 Django 使用一个指定的模板名字,而不是自动生成的默认名字。
    • 我们也为 results 列表视图和 detail 视图指定了 template_name。即使它们在后台都是同一个 DetailView,results 视图和 detail 视图在渲染时具有不同的访问名称。
    • 类似地,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

自动化测试

为什么你需要写测试

  • 测试将节约你的时间:在复杂的应用程序中,组件之间可能会有数十个复杂的交互。改变其中某一组件的行为,也有可能会造成意想不到的结果。判断「代码是否正常工作」意味着你需要用大量的数据来完整的测试全部代码的功能,以确保你的小修改没有对应用整体造成破坏,可想而知其中的工作量。
  • 测试不仅能发现错误且能预防错误:测试是开发的对立面,这种思想是不对的,开发其实更像是一个不断试错的过程。

开始写一个测试程序

  • 约定俗称,Django 应用的测试应该写在应用的 tests.py 文件里,测试系统会自动的在所有以 tests 开头的文件里寻找并执行测试代码。
  • 制造一个 BUG:继续上述提及的应用 Polls,要求 Question 是在一天之内发布, 则 Question.was_published_recently() 方法将会返回 True 。然而现在这个方法在 Question 的 pub_date 字段比当前时间还晚 ( 未来的时间 ) 时也会返回 True。

    1
    2
    3
    4
    5
    6
    7
    8
    import datetime
    from django.utils import timezone
    from polls.models import Question
    # create a Question instance with pub_date 30 days in the future
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time)
    # was it published recently? ==> True
    future_question.was_published_recently()
  • 创造一个测试用例:创建一个 django.test.TestCase 的子类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import datetime
    from django.utils import timezone
    from .models import Question
    from django.test import TestCase

    class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
    """
    was_published_recently() returns False
    for questions whose pub_date is in the future.
    """
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time)
    self.assertIs(future_question.was_published_recently(), False)
  • 最后,我们可通过 PyCharm 单独运行测试用例,也可以通过终端命令运行自动化测试。

    1
    python manage.py test polls
  • 发生了什么呢?以下是自动化测试的运行过程:

    • 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。

参考资料