本文主要内容:简单讨论了项目的背景知识,然后学习Django的基础,包括模型,管理后台,视图,模板和路由等。
前言
在第一节入门中,安装了项目所需要的一切,安装的是 Python3.6,并且在虚拟环境中运行 Django1.11,本文继续在这个项目上编写代码。
本文先讨论一些项目的背景知识,然后在学习Django的基础,包括:模型(models),管理后台(admin),视图(views),模板(templates)和路由(URLs)
论坛项目
通过实际的项目学习知识,是个不错的方法,所以在学习模型,视图等其它有趣部分前,简要的讨论我将要开发的这个项目。
下图关于Web应用程序建模和设计上的一些见解。Web开发和软件开发可不仅仅只是编码。
用例图
这个项目是一个论坛系统,整个项目的构思是维护几个论坛版块(boards),每个版块像一个分类一样。在指定的版块里面,用户可以通过创建新主题(Topic)开始讨论,其他用户可以参与讨论回复。
首先需要找到一种方法来区分普通用户和管理员用户,因为只有管理员可以创建版块。下图概述了主要的用例和每种类型的用户角色:
### 类图
从用例图中,可以开始思考项目所需的实体类有哪些。这些实体就是我们要创建的模型,它与我们的Django应用程序处理的数据非常密切。
为了能够实现上面描述的用例,我们需要至少实现下面几个模型:Board,Topic,Post和User。
- Board:版块
- Topic:主题
- Post:帖子(译注:其实就是主题的回复或评论)
花点时间考虑模型之间如何相互关联也很重要。类与类之间的实线告诉我们,在一个主题(Topic)中,我们需要有一个字段(译注:其实就是通过外键来关联)来确定它属于哪个版块(Board)。同样,帖子(Post)也需要一个字段来表示它属于哪个主题,这样我们就可以列出在特定主题内创建的帖子。最后,我们需要一个字段来表示主题是谁发起的,帖子是谁发的。
用户和版块之间也有联系,谁创建的版块。但是这些信息与应用程序无关。还有其他方法可以跟踪这些信息,稍后会谈到。
现在我们的类图有基本的表现形式,还要考虑这些模型将承载哪些信息。避免让事情变得复杂,试着先把重要的内容列出来,这些内容是启动项目需要的信息。后面我们再使用 Django 的迁移(Migrations)功能来改进模型,将在下一节中详细了解这些内容。
但就目前而言,这是模型最基本的内容:
对于 Board 模型,我们将从两个字段开始:name 和 description。 name字段必须是唯一的,为了避免有重复的名称。description 用于说明这个版块是做什么用的。
Topic 模型包括四个字段:subject 表示主题内容,last_update 用来定义话题的排序,starter 用来识别谁发起的话题,board 用于指定它属于哪个版块。
Post 模型有一个 message 字段,用于存储回复的内容,created_at 在排序时候用(最先发表的帖子排最前面),updated_at 告诉用户是否更新了内容,同时,还需要有对应的 User 模型的引用,Post 由谁创建的和谁更新的。
最后是 User 模型。在类图中,我只提到了字段 username,password,email, is_superuser 标志,因为这几乎是我们现在要使用的所有东西。
需要注意的是,我们不需要创建 User 模型,因为Django已经在contrib包中内置了User模型,我们将直接拿来用。
关于类图之间的对应关系(数字 1,0..* 等等),这里教你如何阅读:
一个topic 必须与一个(1)Board(这意味着它不能为空)相关联,但是 Board 下面可能与许多个或者0个 topic 关联 (0..*)。这意味着 Board 下面可能没有主题。(译注:一对多关系)
一个 Topic 至少有一个 Post(发起话题时,同时会发布一个帖子),并且它也可能有许多 Post(1..*)。一个Post 必须与一个并且只有一个Topic(1)相关联。
一个 Topic 必须有一个且只有一个 User 相关联,topic 的发起者是(1)。而一个用户可能有很多或者没有 topic(0..*)。
Post 必须有一个并且只有一个与之关联的用户,用户可以有许多或没有 Post(0..*)。Post 和 User之间的第二个关联是直接关联(参见该行最后的箭头),就是 Post 可以被用户修改(updated_by),updated_by 有可能是空(Post 没有被修改)
画这个类图的另一种方法是强调字段而不是模型之间的关系:
好了,现在已经够UML了!为了绘制本节介绍的图表,我使用了StarUML 工具。
线框图(原型图)
花了一些时间来设计应用程序的模型后,创建一些线框来定义需要完成的工作,并且清楚地了解我们将要做什么。
基于线框图,我们可以更深入地了解应用程序中涉及的实体。
首先,我们需要在主页上显示所有版块:
“new topic” 页面:
现在,主题页面显示了帖子和讨论:
如果用户点击回复按钮,将看到下面这个页面,并以倒序的方式(最新的在第一个)显示帖子列表:
绘制这些线框,你可以使用draw.io服务,它是免费的。
模型
这些模型基本上代表了应用程序的数据库设计。接下来要做的是创建 Django 所表示的类,这些类是上面建模的类:Board,Topic和Post。User 模型被命名为内置应用叫auth,它以命名空间django.contrib.auth 的形式出现在 INSTALLED_APPS
配置中。
我们要做的工作都在 boards/models.py 文件中。以下是我们在Django应用程序中如何表示类图的代码:
1 | from django.db import models |
所有模型都是django.db.models.Model类的子类。每个类将被转换为数据库表。每个字段由 django.db.models.Field子类(内置在Django core)的实例表示,它们并将被转换为数据库的列。
字段 CharField
,DateTimeField
等等,都是 django.db.models.Field 的子类,包含在Django的核心里面,随时可以使用。
在这里,我们仅使用 CharField
,TextField
,DateTimeField
,和ForeignKey
字段来定义我们的模型。不过在Django提供了更广泛的选择来代表不同类型的数据,例如 IntegerField
,BooleanField
, DecimalField
和其它一些字段。我们会在需要的时候提及它们。
有些字段需要参数,例如CharField
。我们应该始终设定一个 max_length。这些信息将用于创建数据库列。Django需要知道数据库列需要多大。该max_length
参数也将被Django Forms API用来验证用户输入。
在Board
模型定义中,更具体地说,在name
字段中,我们设置了参数 unique=True
,顾名思义,它将强制数据库级别字段的唯一性。
在Post
模型中,created_at
字段有一个可选参数,auto_now_add
设置为True
。这将告诉Django创建Post
对象时为当前日期和时间。
模型之间的关系使用ForeignKey
字段。它将在模型之间创建一个连接,并在数据库级别创建适当的关系(译注:外键关联)。该ForeignKey
字段需要一个位置参数related_name
,用于引用它关联的模型。(译注:例如 created_by
是外键字段,关联的User模型,表明这个帖子是谁创建的,related_name=posts
表示在User
那边可以使用 user.posts
来查看这个用户创建了哪些帖子)
例如,在Topic
模型中,board
字段是Board
模型的ForeignKey
。它告诉Django,一个Topic
实例只涉及一个Board
实例。related_name
参数将用于创建反向关系,Board
实例通过属性topics
访问属于这个版块下的Topic
列表。
Django自动创建这种反向关系,related_name
是可选项。但是,如果我们不为它设置一个名称,Django会自动生成它:(class_name)_set
。例如,在Board
模型中,所有Topic
列表将用topic_set
属性表示。而这里我们将其重新命名为了topics
,以使其感觉更自然。
在Post
模型中,该updated_by
字段设置related_name='+'
。这指示Django我们不需要这种反向关系,所以它会被忽略(译注:也就是说我们不需要关系用户修改过哪些帖子)。
下面可以看到类图和Django模型的源代码之间的比较,绿线表示我们如何处理反向关系。
这时,你可能会问自己:“主键/ ID呢?”?如果我们没有为模型指定主键,Django会自动为我们生成它。所以现在一切正常。在下一节中,您将看到它是如何工作的。
迁移模型
下一步是告诉Django创建数据库,以便我们可以开始使用它。
打开终端 ,激活虚拟环境,转到 manage.py文件所在的文件夹,然后运行以下命令:
python manage.py makemigrations
Migrations for 'boards':
boards/migrations/0001_initial.py
- Create model Board
- Create model Post
- Create model Topic
- Add field topic to post
- Add field updated_by to post
如图:
此时,Django 在 boards/migrations
目录创建了一个名为 0001_initial.py
的文件。它代表了应用程序模型的当前状态。再下一步,Django将使用该文件创建表和列。
迁移文件将被翻译成SQL语句。如果您熟悉SQL,则可以运行以下命令来检验将是要被数据库执行的SQL指令
python manage.py sqlmigrate boards 0001
如果你不熟悉SQL,也不要担心。在本系列教程中,我们不会直接使用SQL。所有的工作都将使用Django ORM来完成,它是一个与数据库进行通信的抽象层。
下一步是将我们生成的迁移文件应用到数据库:
python manage.py migrate
因为这是我们第一次迁移数据库,所以migrate
命令把Django contrib app 中现有的迁移文件也执行了,这些内置app列在了INSTALLED_APPS
。这是预料之中的。
Applying boards.0001_initial... OK
是我们在上一步中生成的迁移脚本
好了!我们的数据库已经可以使用了。
需要注意的是SQLite是一个产品级数据库。SQLite被许多公司用于成千上万的产品,如所有Android和iOS设备,主流的Web浏览器,Windows 10,MacOS等。
但这不适合所有情况。SQLite不能与MySQL,PostgreSQL或Oracle等数据库进行比较。大容量的网站,密集型写入的应用程序,大的数据集,高并发性的应用使用SQLite最终都会导致问题。
我们将在开发项目期间使用SQLite,因为它很方便,不需要安装其他任何东西。当我们将项目部署到生产环境时,再将切换到PostgreSQL(译注:后续,我们后面可能使用MySQL)。对于简单的网站这种做法没什么问题。但对于复杂的网站,建议在开发和生产中使用相同的数据库。
试验Models API
使用Python进行开发的一个重要优点是交互式shell,这是一种快速尝试和试验API的方法
可以使用manage.py 工具加载我们的项目来启动 Python shell :
python manage.py shell
这与直接输入python
指令来调用交互式控制台是非常相似的,除此之外,项目将被添加到sys.path
并加载Django。这意味着我们可以在项目中导入我们的模型和其他资源并使用它。
让我们从导入Board类开始:
1 | from boards.models import Board |
要创建新的 boarrd 对象,我们可以执行以下操作:
1 | board = Board(name='Django', description='This is a board about Django.') |
为了将这个对象保存在数据库中,我们必须调用save方法:
1 | board.save() |
save
方法用于创建和更新对象。这里Django创建了一个新对象,因为这时Board 实例没有id。第一次保存后,Django会自动设置ID:
board.id
1
可以将其余的字段当做Python属性访问:
如图:
要更新一个值,我们可以这样做:
1 | board.description = 'Django discussion board.' |
每个Django模型都带有一个特殊的属性; 我们称之为模型管理器(Model Manager)。你可以通过属性objects
来访问这个管理器,它主要用于数据库操作。例如,我们可以使用它来直接创建一个新的Board对象:
1 | board = Board.objects.create(name='Python', description='General discussion about Python.') |
访问属性:
所以,现在我们有两个版块了。我们可以使用objects
列出数据库中所有现有的版块:
1 | Board.objects.all() |
1 | Board.objects.all() |
结果是一个QuerySet
。稍后我们会进一步了解。基本上,它是从数据库中查询的对象列表。我们看到有两个对象,但显示的名称是 Board object。这是因为我们尚未实现 Board 的__str__
方法。
__str__
方法是对象的字符串表示形式。我们可以使用版块的名称来表示它。
首先,退出交互式控制台:
1 | exit() |
现在编辑boards app 中的 models.py 文件:
1 | class Board(models.Model): |
重新查询,再次进入交互式控制台:
1 | from boards.models import Board |
我们可以将这个QuerySet看作一个列表。假设我们想遍历它并打印每个版块的描述:
1 | boards_list = Board.objects.all() |
同样,我们可以使用模型的 管理器(Manager) 来查询数据库并返回单个对象。为此,我们要使用 get
方法:
1 | 1) django_board = Board.objects.get(id= |
但我们必须小心这种操作。如果我们试图查找一个不存在的对象,例如,查找id=3的版块,它会引发一个异常:
1 | 3) board = Board.objects.get(id= |
get
方法的参数可以是模型的任何字段,但最好使用可唯一标识对象的字段来查询。否则,查询可能会返回多个对象,这也会导致异常。
1 | 'Django') Board.objects.get(name= |
请注意,查询区分大小写,小写“django”不匹配:
操作 | 代码示例 |
---|---|
创建一个对象而不保存 | board = Board() |
保存一个对象(创建或更新) | board.save() |
数据库中创建并保存一个对象 | Board.objects.create(name='...',description='...') |
列出所有对象 | Board.objects.all() |
通过字段标识获取单个对象 | Board.objects.get(id=1) |
视图,模板,静态文件
目前我们已经有一个视图函数叫home,这个视图在我们的应用程序主页上显示为“Hello,World!”
myproject/urls.py
1 | from django.conf.urls import url |
boards/views.py
1 | from django.http import HttpResponse |
我们可以从这里开始写。如果你回想起我们的原型图,图5显示了主页应该是什么样子。我们想要做的是在表格中列出一些版块的名单以及它们的描述信息。
首先要做的是导入Board模型并列出所有的版块
1 | from django.http import HttpResponse |
运行结果就是这个简单的HTML页面:
真正的项目里面我们不会这样去渲染HTML。对于这个简单视图函数,我们做的就是列出所有版块,然后渲染部分是Django模板引擎的职责。
Django模板引擎设置
在manage.py所在的目录创建一个名为 templates的新文件夹:
myproject/
|-- myproject/
| |-- boards/
| |-- myproject/
| |-- templates/ <-- 这里
| +-- manage.py
+-- venv/
在templates文件夹中,创建一个名为home.html的HTML文件:
templates/home.html
1 |
|
在上面的例子中,我们混入了原始HTML和一些特殊标签 {% for ... in ... %}
和{{ variable }}
。它们是Django模板语言的一部分。上面的例子展示了如何使用 for
遍历列表对象。{{ board.name }}
会在 HTML 模板中会被渲染成版块的名称,最后生成动态HTML文档。
在我们可以使用这个HTML页面之前,我们必须告诉Django在哪里可以找到我们应用程序的模板。
打开myproject目录下面的settings.py文件,搜索TEMPLATES
变量,并设置DIRS
的值为os.path.join(BASE_DIR, 'templates')
1 | TEMPLATES = [ |
本质上,刚添加的这一行所做的事情就是找到项目的完整路径并在后面附加“/templates”
我们可以使用Python shell进行调试
python manage.py shell
1 | from django.conf import settings |
从上面看到,添加部分只是指向我们在前面步骤中创建的templates文件夹
现在我们可以更新home视图:
boards/views.py
1 | from django.shortcuts import render |
生成HTML;
我们可以用一个更漂亮的表格来替换,改进HTML模板:
templates/home.html
1 |
|
测试主页
测试将是一个反复出现的主题,我将在学习整个教程系列中探讨不同的概念和策略。
开始写第一个测试。现在,我们将在boards应用程序内的tests.py文件中操作
boards/tests.py
1 | from django.core.urlresolvers import reverse |
这是一个非常简单但非常有用的测试用例,我们测试的是请求该URL后返回的响应状态码。状态码200意味着成功。
请求一下主页后,我们可以在控制台中看到响应的状态代码:
如果出现未捕获的异常,语法错误或其他任何情况,Django会返回状态代码500,这意味着是内部服务器错误。现在,想象我们的应用程序有100个视图函数。如果我们为所有视图编写这个简单的测试,只需一个命令,我们就能够测试所有视图是否返回成功代码,因此用户在任何地方都看不到任何错误消息。如果没有自动化测试,我们需要逐一检查每个页面是否有错误。
执行Django的测试套件:
python manage.py test
测试结果:
现在我们可以测试Django是否在请求的URL的时候返回了正确的视图函数。这也是一个有用的测试,因为随着开发的进展,您会发现urls.py模块可能变得非常庞大而复杂。URL conf 全部是关于解析正则表达式的。有些情况下有一个非常宽容的URL(译注:本来不应该匹配的,却因为正则表达式写的过于宽泛而错误的匹配了),所以Django最终可能返回错误的视图函数。
我们可以这样做:
boards/tests.py
1 | from django.core.urlresolvers import reverse |
在第二个测试中,我们使用了resolve
函数。Django使用它来将浏览器发起请求的URL与urls.py模块中列出的URL进行匹配。该测试用于确定URL /
返回 home 视图。
再次测试:
python manage.py test
要查看有关测试执行时更详细的信息,可将verbosity的级别设置得更高一点:
python manage.py test --verbosity=2
测试结果:
verbosity决定了将要打印到控制台的通知和调试信息量; 0是无输出,1是正常输出,2是详细输出。
静态文件设置
静态文件是指 CSS,JavaScript,字体,图片或者是用来组成用户界面的任何其他资源。
实际上,Django 本身是不负责处理这些文件的,但是为了让我们的开发过程更轻松,Django 提供了一些功能来帮助我们管理静态文件。这些功能可在 INSTALLED_APPS
的 django.contrib.staticfiles 应用程序中找到(译者:Django为了使得开发方便,也可以处理静态文件,而在生产环境下,静态文件一般直接由 Nginx 等反向代理服务器处理,而应用服务器专心负责处理它擅长的业务逻辑)
市面上很多优秀前端组件框架,我们没有理由继续用简陋的HTML文档来渲染。我们可以轻松地将Bootstrap 4添加到我们的项目中。Bootstrap是一个用HTML,CSS和JavaScript开发的前端开源工具包。
在项目根目录中,除了boards, templates 和myproject文件夹外,再创建一个名为static的新文件夹,并在static文件夹内创建另一个名为css的文件夹:
myproject/
|-- myproject/
| |-- boards/
| |-- myproject/
| |-- templates/
| |-- static/ <-- here
| | +-- css/ <-- and here
| +-- manage.py
+-- venv/
转到getbootstrap.com并下载最新版本:
下载编译版本的CSS和JS
在你的计算机中,解压 bootstrap-4.0.0-beta-dist.zip 文件,将文件 css/bootstrap.min.css 复制到我们项目的css文件夹中:
myproject/
|-- myproject/
| |-- boards/
| |-- myproject/
| |-- templates/
| |-- static/
| | +-- css/
| | +-- bootstrap.min.css <-- here
| +-- manage.py
+-- venv/
下一步是告诉Django在哪里可以找到静态文件。打开settings.py,拉到文件的底部,在STATIC_URL后面添加以下内容:
1 | STATIC_URL = '/static/' |
TEMPLATES目录和这个配置是一样的
必须在模板中加载静态文件(Bootstrap CSS文件):
templates/home.html
1 | {% load static %} |
首先,我们在模板的开头使用了 Static Files App 模板标签 {% load static %}
。
模板标签{% 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
但凡是需要引用CSS,JavaScript或图片文件的地方就使用{% static %}
。稍后,当我们开始部署项目到正式环境时,我们将讨论更多。现在都设置好了。
刷新页面 http://127.0.0.1:8000 ,我们可以看到它可以正常运行:
现在我们可以编辑模板,以利用Bootstrap CSS:
1 | {% load static %} |
显示效果:
到目前为止,我们使用交互式控制台(python manage.py shell)添加了几个新的版块。但我们需要一个更好的方式来实现。接下来,我们将为网站管理员实现一个管理界面来管理这些数据。
Django Admin介绍
当我们开始一个新项目时,Django已经配置了Django Admin,这个应用程序列出的INSTALLED_APPS
。
使用 Django Admin的一个很好的例子就是用在博客中; 它可以被作者用来编写和发布文章。另一个例子是电子商务网站,工作人员可以创建,编辑,删除产品。
现在,我们将配置 Django Admin 来维护我们应用程序的版块。
我们首先创建一个管理员帐户:
python manage.py createsuperuser
按照说明操作:
1 | Username (leave blank to use 'vitorfs'): admin |
在浏览器中打开该URL:http://127.0.0.1:8000/admin/
输入用户名和密码登录到管理界面:
它已经配置了一些功能。在这里,我们可以添加用户和组的权限管理,这些概念在后面我们将探讨更多。
添加Board模型非常简单。打开boards目录中的admin.py文件,并添加以下代码:
boards/admin.py
1 | from django.contrib import admin |
保存admin.py文件,然后刷新网页浏览器中的页面:
对!它已准备好被使用了。点击Boards链接查看现有版块列表:
我们可以通过点击 Add Board 按钮添加一个新的版块:
点击保存按钮:
我们可以检查一切是否正常,打开URL http://127.0.0.1:8000
总结
在本教程中,我们探讨学习了许多新概念。我们为项目定义了一些需求,创建了第一个模型,迁移了数据库,开始玩 Models API。我们创建了第一个视图并编写了一些单元测试。同时我们还配置了Django模板引擎,静态文件,并将Bootstrap 4库添加到项目中。最后,我们简要介绍了Django Admin界面。
这是学习的基础部分!下一部分,将学习探索Django的URL路由,表单API,可重用模板以及更多测试。