本文主要介绍 Django 中 Model, Views, Templates 和 Static Files, 并简单引入了 Django Admin.

本系列是根据 Vitor Freitas 的Django教程整理而来,如有疑问可参考 原文

点击查看索引

我们的目标是建立一个 Forum(论坛) ,其中包括 Board(板块)Topic(话题)Post(跟帖)User(用户) 四个 Class
这部分原文有十分详细的介绍,此处不再赘述。
其中 User 已经在 Django 的内置 app auth 定义,因此我们只需要定义其余三个。

Models

定义 Models

boards/models.py 中加入以下代码:

from django.db import models
from django.contrib.auth.models import User


class Board(models.Model):
    name = models.CharField(max_length=30, unique=True)
    description = models.CharField(max_length=100)


class Topic(models.Model):
    subject = models.CharField(max_length=255)
    last_updated = models.DateTimeField(auto_now_add=True)
    board = models.ForeignKey(Board, related_name='topics', on_delete=models.CASCADE)
    starter = models.ForeignKey(User, related_name='topics', on_delete=models.CASCADE)


class Post(models.Model):
    message = models.TextField(max_length=4000)
    topic = models.ForeignKey(Topic, related_name='posts', on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(null=True)
    created_by = models.ForeignKey(User, related_name='posts', on_delete=models.CASCADE)
    updated_by = models.ForeignKey(User, null=True, related_name='+', on_delete=models.CASCADE)

所有 model 都是 django.db.models.Model 类的子类。每个 class 都将转换为数据库表。每个 field 由 django.db.models.Field 子类的实例表示,并将被转换为数据库列。

CharFieldDateTimeField 等 field 都是 django.db.models.Field 的子类,它们包含在 Django 核心中——随时可用。

某些 field 具有必需的参数,例如 CharField 。我们应该 始终 设置一个 max_length 。此信息将用于创建数据库列。 Django 需要知道数据库列需要多大。 Django Forms API 也将使用 max_length 参数来验证用户输入。

在 Board 定义中,更具体地说,在 name field 中,我们还设置了参数 unique=True,顾名思义,它将在数据库级别 强制该 field 的 唯一性

在 Post 中, created_at field 有一个可选参数, auto_now_add 设置为 True。这将指示 Django 在创建 Post 对象时 设置当前日期和时间

在 model 之间 创建关系 的一种方法是使用 ForeignKey 。它将在 model 之间创建链接并在数据库级别创建适当的关系。 ForeignKey field 需要一个位置参数,其中包含对其相关模型的引用。
例如,在 Topic model 中,board field 是 Board model 的 ForeignKey 。它告诉 Django 一个 Topic 实例只与一个 Board 实例相关。 related_name 参数将用于创建 反向关系 ,其中 Board 实例将有权访问属于它的 Topic 实例列表。
Django 自动创建这种反向关系,也就是说, related_name 是可选的。但是如果我们不为它设置名称,Django 将使用名称生成它:(class_name)_set。例如,在 Board model 中,Topic 实例将在 topic_set 属性下可用。我们只是将其 重命名topics ,以使其感觉更自然。
在 Post 模型中,updated_by 字段设置了 related_name=’+’` 。这告诉 Django 我们不需要这种反向关系,所以它会忽略它。

注意:在 Django2.0 后,定义 ForeignKeyOneToOneField 时需要加上 on_delete 参数,此参数为了避免两个表里的数据不一致问题,不然会报错:
TypeError: init() missing 1 required positional argument: ‘on_delete’

迁移 Models

下一步是告诉 Django 创建数据库,以便我们可以使用它。
manage.py 所在目录打开 Terminal,运行以下命令:

python manage.py makemigrations

此时,Django 在 board/migrations 目录中创建了一个名为 0001_initial.py 的文件。它代表我们应用程序模型的当前状态。在下一步中,Django 将使用这个文件来创建表和列。迁移文件被翻译成 SQL 语句。
现在下一步是将我们生成的迁移应用到数据库:

python manage.py migrate

输出会类似于:

Operations to perform:
  Apply all migrations: admin, auth, boards, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying boards.0001_initial... OK
  Applying sessions.0001_initial... OK

因为这是我们第一次迁移数据库,所以 migrate 命令还应用了来自 Django contrib apps 的现有迁移文件,在 INSTALLED_APPS 中列出。
Applying board.0001_initial... OK 就是我们在上一步生成的迁移。

使用 Models API

使用以下命令打开 Python Shell

python manage.py shell

会进入如下界面:

Python Shell


在 Python Shell 中,Ctrl+C 会中断当前的 Python 任务,而不会退出 Python Shell,如果想要退出 Python Shell,需要输入命令:

exit()

或使用快捷键 Ctrl+D

在 Python Shell 中输入以下命令以导入 Board class:

from boards.models import Board

使用以下命令创建新的 board 实例:

board = Board(name='Django', description='This is a board about Django.')

使用以下命令保存该实例于数据库中:

board.save()

save 方法用于创建和更新对象。在这里 Django 创建了一个新对象,因为 Board 实例没有 id。第一次保存后,Django会自动设置id:

Board id

也可通过 board.nameboard.description 访问其他 field。

通过以下命令可以更新 field 的值:

board.description = 'Django discussion board.'
board.save()

我们也可以通过如下命令创建一个新的 board 实例:

board = Board.objects.create(name='Python', description='General discussion about Python.')

输入以下命令得到所有已存在的 boards:

Board.objects.all()

输出如下:

<QuerySet [<Board: Board object (1)>, <Board: Board object (2)>]>

这个结果是一个 QuerySet ,我们可以看到我们有两个对象,但我们只能读取 Board object 。那是因为我们没有在 Board model 中定义 __str__ method。

首先我们需要退出 Python Shell
然后编辑 boards 中的 models.py

class Board(models.Model):
    name = models.CharField(max_length=30, unique=True)
    description = models.CharField(max_length=100)

    def __str__(self):
        return self.name

这时再进行上面的步骤,就会得到:

输出

对于这个 QuerySet ,其具有类似 list 的某些用法,此处不再赘述。
我们可以使用 model Manager 来查询数据库并返回单个对象。为此,我们使用 get method:

django_board = Board.objects.get(id=1)

django_board.name

输出:

'Django'

但是我们必须谨慎操作。如果我们试图获取一个不存在的对象,例如,一个 id=3 的 board,它会引发 exception。
我们可以对任何 model field 使用 get method,但最好使用可以 唯一标识对象 的 field。否则,查询可能会返回多个对象,这将导致异常。
需要注意的是,get 查询是 区分大小写 的。

Model’s Operations 总结

下面是我们在本节中学习的方法和操作的摘要,使用 Board model 作为参考。大写的 Board 指的是 class,小写的 board 指的是 Board model class 的一个 instance/object:

Operation 代码示例
创建 object 而不保存 board = Board()
保存 object(创建或更新) board.save()
在数据库中创建并保存 object Board.objects.create(name=’…’, description=’…’)
列出所有 object Board.objects.all()
获取由 field 标识的单个object Board.objects.get(id=1)

Views, Templates 和 Static Files

目前我们已经有一个名为 home 的 view,显示“Hello, World!”在我们的 app 的 homepage 上。现在我们想要做的是在表格中显示一个 board 列表及一些其他信息。
首先要做的是导入 Board model 并列出所有现有的 boards:
boards/views.py

from django.http import HttpResponse
from .models import Board

def home(request):
    boards = Board.objects.all()
    boards_names = list()

    for board in boards:
        boards_names.append(board.name)

    response_html = '<br>'.join(boards_names)

    return HttpResponse(response_html)

运行服务器,我们会看到:

运行结果

但是,我们不会像这样渲染 HTML。我们只需要一个 broads 列表,渲染部分是 Django Template Engine (Django 模板引擎)的工作。

Django Template Engine

在 manage.py 和 boards 文件夹同级目录创建一个名为 templates 的新文件夹:

  • myproject/
    • manage.py
    • myproject/
    • boards/
    • templates/ <– here!

然后在 templates 文件夹中,创建一个名为 home.html 的 HTML 文件:
templates/home.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Boards</title>
  </head>
  <body>
    <h1>Boards</h1>

    {% for board in boards %}
      {{ board.name }} <br>
    {% endfor %}

  </body>
</html>

在上面的代码中,我们将原始 HTML 与一些特殊标签 {% for ... in ... %}{{ variable }} 混合在一起。这些特殊标签是 Django Template 语言的一部分。上面的示例显示了如何使用 for 遍历对象列表。 {{ board.name }} 在 HTML 中呈现 board 的名称,生成动态的 HTML 文档。
在使用这个 HTML 页面之前,我们必须告诉 Django 在哪里可以找到我们的app 的 template。
打开 myproject 目录中的 settings.py,搜索 TEMPLATES 变量并将 DIRS 键设置为 os.path.join(BASE_DIR, ‘templates’):

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(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',
            ],
        },
    },
]

这行代码所做的是找到项目目录的完整路径并将“/templates”附加到它后面。使用 Python Shell 可以更直观地理解:

运行结果

现在我们可以更新我们的主页视图:
boards/views.py

from django.shortcuts import render
from .models import Board

def home(request):
    boards = Board.objects.all()
    return render(request, 'home.html', {'boards': boards})

运行结果

我们可以使用表格来改进 HTML 模板:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Boards</title>
  </head>
  <body>
    <h1>Boards</h1>

    <table border="1">
      <thead>
        <tr>
          <th>Board</th>
          <th>Posts</th>
          <th>Topics</th>
          <th>Last Post</th>
        </tr>
      </thead>
      <tbody>
        {% for board in boards %}
          <tr>
            <td>
              {{ board.name }}<br>
              <small style="color: #888">{{ board.description }}</small>
            </td>
            <td>0</td>
            <td>0</td>
            <td></td>
          </tr>
        {% endfor %}
      </tbody>
    </table>
  </body>
</html>

运行结果

Testing the Homepage

让我们编写我们的第一个 test。现在,我们将在 board app 中的 tests.py 文件中进行编辑:
boards/tests.py

from django.urls import reverse
from django.test import TestCase

class HomeTests(TestCase):
    def test_home_view_status_code(self):
        url = reverse('home')
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

这是一个非常简单的测试用例,但十分有用。我们正在测试响应的 status code(状态码)。status code 显示为 200 时表示 成功

运行结果

如果有未捕获的异常、语法错误或其他任何情况,Django 将返回 status code 500,这意味着 内部服务器错误 。现在,假设我们的应用程序有 100 个 views。我们只用一个命令为所有 views 编写这个简单的测试,我们将能够测试是否所有视图都返回成功代码。如果没有自动化测试,我们将需要一一检查每一页。
执行 Django 的 test 套件:

python manage.py test

运行结果:

运行结果

现在我们可以测试 Django 是否为请求的 URL 返回了正确的 view function。这也是一个有用的测试,因为随着我们的开发进展,urls.py module 会变得非常大与复杂。 URL conf 是解析正则表达式的,在某些情况下,我们有一个非常宽松的 URL,因此 Django 最终可能会返回错误的 view function。下面是实现测试的代码:
boards/tests.py

from django.urls import reverse
from django.urls import resolve
from django.test import TestCase
from .views import home

class HomeTests(TestCase):
    def test_home_view_status_code(self):
        url = reverse('home')
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_home_url_resolves_home_view(self):
        view = resolve('/')
        self.assertEquals(view.func, home)

运行结果:

运行结果

在第二个测试中,我们使用了 resolve 函数。 Django 使用它将请求的 URL 与 urls.py module 中列出的 URL 列表进行匹配。此测试将确保作为根 URL 的 / 返回 home view。
如果想查看有关测试执行的更多详细信息,需要将详细程度设置为更高级别:

python manage.py test --verbosity=2

运行结果:

运行结果

详细程度决定了将输出到控制台的通知和调试信息的数量: 0 为无输出,1 为正常输出,2 为详细输出。

Static Files

Static Files (静态文件)是 CSS、JavaScript、字体、图像或可能用来组成用户界面的任何其他资源。
Django 提供了一些功能来帮助我们管理静态文件。这些功能在 INSTALLED_APPS 配置中已经列出的 django.contrib.staticfiles app 中。
有如此多可用的前端组件库,我们没有理由继续渲染基本的 HTML 文档。我们可以轻松地将 Bootstrap 4 添加到我们的项目中。 Bootstrap 是一个开源工具包,其用于使用HTML, CSS, 和 JavaScript所开发的项目。

在 manage.py 和 boards 文件夹同级目录创建一个名为 static 的新文件夹,然后在 static 文件夹中创建一个名为 css 的文件夹:

  • myproject/
    • manage.py
    • myproject/
    • boards/
    • templates/
    • static/ <– here!
      • css/ <– and here!

访问 Bootstrap 官网 并下载最新版本的 Compiled CSS and JS version

下载界面

解压从 Bootstrap 网站下载的 bootstrap-x.x.x-dist.zip 文件,将文件 css/bootstrap.min.css 复制到我们项目的 css 文件夹中:

  • myproject/
    • manage.py
    • myproject/
    • boards/
    • templates/
    • static/
      • css/
        • bootstrap.min.css <– here!

下一步是指示 Django 在哪里可以找到 static files。打开 settings.py ,在文件底部,STATIC_URL 之后,添加以下内容:

STATIC_URL = '/static/'

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

这其实与我们在 Templates 处的设置 方法相同。
现在我们要在 template 中加载 static files(Bootstrap CSS 文件):
templates/home.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Boards</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
  </head>
  <body>
    <!-- body suppressed for brevity ... -->
  </body>
</html>

首先,我们使用 template 开头的 {% load static %} 加载 Static Files App template 标签。
{% static %} 用于组成资源所在的 URL。在此情况下,{% static 'css/bootstrap.min.css' %} 将返回 /static/css/bootstrap.min.css ,相当于 http://127.0.0.1:8000/static/css/bootstrap.min.css
{% static %} 使用 settings.py 中的 STATIC_URL 配置来组成最终的 URL。例如,如果静态文件托管在 https://static.example.com/ 等子域中,我们将设置 STATIC_URL=https://static.example.com/ ,然后 {% static 'css/bootstrap. min.css' %} 将返回 https://static.example.com/css/bootstrap.min.css
现在我们可以编辑模板以利用 Bootstrap CSS:
templates/home.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Boards</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
  </head>
  <body>
    <div class="container">
      <ol class="breadcrumb my-4">
        <li class="breadcrumb-item active">Boards</li>
      </ol>
      <table class="table">
        <thead class="thead-inverse">
          <tr>
            <th>Board</th>
            <th>Posts</th>
            <th>Topics</th>
            <th>Last Post</th>
          </tr>
        </thead>
        <tbody>
          {% for board in boards %}
            <tr>
              <td>
                {{ board.name }}
                <small class="text-muted d-block">{{ board.description }}</small>
              </td>
              <td class="align-middle">0</td>
              <td class="align-middle">0</td>
              <td></td>
            </tr>
          {% endfor %}
        </tbody>
      </table>
    </div>
  </body>
</html>

运行结果如下:

运行结果

Django Admin 简介

到目前为止,我们正在使用交互式控制台(python manage.py shell)添加新的 broads。但我们需要一种更好的方法。我们将实现一个管理界面,供网站管理员管理。
当我们开始一个新项目时,Django 已经配置了 INSTALLED_APPS 中列出的 Django Admin
Django Admin 的一个很好的用例是在博客中:作者可以使用它来撰写和发表文章。另一个例子是电子商务网站:工作人员可以在其中创建、编辑、删除产品。
现在,我们将配置 Django Admin 来维护我们的 app 的 boards。
让我们从创建 Admin 帐户开始:

python manage.py createsuperuser

填写相应的内容后,运行我们的服务器并打开 http://127.0.0.1:8000/admin/
可以看到 Admin 的登录界面:

Admin 登陆界面

登录后的界面:

Admin 界面

它已经配置了一些功能。在这里我们可以添加 user 和 group 来管理权限。稍后我们将探讨更多这些概念。
添加 Board model 非常简单。打开boards目录下的admin.py文件,添加如下代码:
boards/admin.py

from django.contrib import admin
from .models import Board

admin.site.register(Board)

保存 admin.py 文件,然后在浏览器上刷新页面,即可看到:

Admin 界面

之后我们即可在此界面对网站进行管理。