本项目来着 Django 2 By Example 翻译 柚爸

创建博客应用

本文将介绍如何创建可以用于生产环境的完整Django项目。如果你还没有安装Django,本章在第一部分中将介绍如何安装Django,之后的内容还包括创建一个简单的博客应用。本章的目的是让读者对Django的整体运行有一个概念,理解Django的各个组件如何交互运作,知道创建一个应用的基础方法。本书将指导你创建一个个完整的项目,但不会对所有细节进行详细阐述,Django各个组件的内容会在全书的各个部分内进行解释。

本章的主要内容有:

  • 安装Django并创建第一个项目
  • 设计数据模型和进行模型迁移(migrations)
  • 为数据模型创建管理后台
  • 使用QuerySet和模型管理器
  • 创建视图、模板和URLs
  • 给列表功能的视图添加分页功能
  • 使用基于类的视图

安装Django

如果已经安装了Django,可以跳过本部分到创建第一个Django项目小节。Django是Python的一个包(模块),所以可以安装在任何Python环境。如果还没有安装Django,本节是一个用于本地开发的快速安装Django指南。

Django 2.0需要Python解释器的版本为3.4或更高。在本书中,采用Python 3.6.5版本,如果使用Linux或者macOS X,系统中也许已经安装Python(部分Liunx发行版初始安装Python2.7),对于Windows系统,从https://www.python.org/downloads/windows/下载Python安装包。

译者在此强烈建议使用基于UNIX的系统进行开发。

如果不确定系统中是否已经安装了Python,可以尝试在系统命令行中输入python然后查看输出结果,如果看到类似下列信息,则说明Python已经安装:

Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 03:03:55)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

如果安装的版本低于3.4,或者没有安装Python,从https://www.python.org/downloads/下载并安装。

由于我们使用Python 3,所以暂时不需要安装数据库,因为Python 3自带一个轻量级的SQLite3数据库可以用于Django开发。如果打算在生产环境中部署Django项目,需要使用更高级的数据库,比如PostgreSQL,MySQL或者Oracle数据库。关于如何在Django中使用数据库,可以看官方文档https://docs.djangoproject.com/en/2.0/topics/install/#database-installation

译者注:在翻译本书和实验代码的时候,译者的开发环境是Centos 7.5 1804 + Python 3.7.0 + Django 2.1.0(最后一章升级到Django 2.1.2),除了后文会提到的一个旧版本第三方库插件冲突的问题之外,未发现任何兼容性问题。

创建独立的Python开发环境

推荐使用virtualenv创建独立的开发环境,这样可以对不同的项目应用不同版本的模块,比将这些模块直接安装为系统Python的第三方库要灵活很多。另一个使用virtualenv的优点是安装Python模块的时候不需要任何管理员权限。在系统命令行中输入如下命令来安装virtualenv

pip install virtualenv

在安装完virtualenv之后,通过以下命令创建一个独立环境:

virtualenv my_env

译者注:需要将virtualenv的所在路径添加到系统环境变量PATH中,对于Django也是如此,不然无法直接执行django-admin命令。

这个命令会在当前目录下创建一个my_env/目录,其中放着一个Python虚拟环境。在虚拟环境中安装的Python包实际会被安装到my_env/lib/python3.6/site-packages目录中。

如果操作系统中安装的是Python 2.X,必须再安装Python 3.X,还需要设置virtualenv虚拟Python 3.X的环境。

可以通过如下命令查找Python 3的安装路径,然后创建虚拟环境:

$ which python3
/Library/Frameworks/Python.framework/Versions/3.6/bin/python3
$ virtualenv my_env -p /Library/Frameworks/Python.framework/Versions/3.6/bin/python3

根据Linux发行版的不同,上边的代码也会有所不同。在创建了虚拟环境对应的目录之后,使用如下命令激活虚拟环境:

source my_env/bin/activate

激活之后,在命令行模式的提示符前会显示括号包住该虚拟环境的名称,如下所示:

(my_env)laptop:~ $

开启虚拟环境后,随时可以通过在命令行中输入deactivate来退出虚拟环境。

关于virtualenv的更多内容可以查看https://virtualenv.pypa.io/en/latest/

virtualenvwrapper这个工具可以方便的创建和管理系统中的所有虚拟环境,需要在系统中先安装virtualenv,可以到https://virtualenvwrapper.readthedocs.io/en/latest/下载。

使用PIP安装Django

推荐使用pip包安装Django。Python 3.6已经预装了pip,也可以在https://pip.pypa.io/en/stable/installing/找到pip的安装指南。

使用下边的命令安装Django:

pip install Django==2.0.5

译者这里安装的是2.1版。

Django会被安装到虚拟环境下的site-packages/目录中。

现在可以检查Django是否已经成功安装,在系统命令行模式运行python,然后导入Django,检查版本,如下:

>>> import django
>>> django.get_version()
'2.0.5'

如果看到了这个输出,就说明Django已经成功安装了。

Django的其他安装方式,可以查看官方文档完整的安装指南:https://docs.djangoproject.com/en/2.0/topics/install/

创建第一个Django项目

本书的第一个项目是创建一个完整的博客项目。Django提供了一个创建项目并且初始化其中目录结构和文件的命令,在命令行模式中输入:

django-admin startproject mysite

这会创建一个项目,名称叫做mysite

避免使用Python或Django的内置名称作为项目名称。

看一下项目目录的结构:

mysite/
  manage.py
  mysite/
    __init__.py
    settings.py
    urls.py
    wsgi.py

这些文件解释如下:

  • manage.py:是一个命令行工具,可以通过这个文件管理项目。其实是一个django-admin.py的包装器,这个文件在创建项目过程中不需要编辑。
  • mysite/:这是项目目录,由以下文件组成:
    • __init__.py:一个空文件,告诉Python将mysite看成一个包。
    • settings.py:这是当前项目的设置文件,包含一些初始设置
    • urls.py:这是URL patterns的所在地,其中的每一行URL,表示URL地址与视图的一对一映射关系。
    • wsgi.py:这是自动生成的当前项目的WSGI程序,用于将项目作为一个WSGI程序启动。

自动生成的settings.py是当前项目的配置文件,包含一个用于使用SQLite 3 数据库的设置,以及一个叫做INSTALLED_APPS的列表。INSTALLED_APPS包含Django默认添加到一个新项目中的所有应用。在之后的项目设置部分会接触到这些应用。

为了完成项目创建,还必须在数据库里创建起INSTALLED_APPS中的应用所需的数据表,打开系统命令行输入下列命令:

cd mysite
python manage.py migrate

会看到如下输出:

Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... 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 sessions.0001_initial... OK

这些输出表示Django刚刚执行的数据库迁移(migrate)工作,在数据库中创建了这些应用所需的数据表。在本章的创建和执行迁移部分会详细介绍migrate命令。

运行开发中的站点

Django提供了一个轻量级的Web服务程序,无需在生产环境即可快速测试开发中的站点。启动这个服务之后,会检查所有的代码是否正确,还可以在代码被修改之后,自动重新载入修改后的代码,但部分情况下比如向项目中加入了新的文件,还需要手工关闭服务再重新启动。

在命令行中输入下列命令就可以启动站点:

python manage.py runserver

应该会看到下列输出:

Performing system checks...

System check identified no issues (0 silenced).
May 06, 2018 - 17:17:31
Django version 2.0.5, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

现在可以在浏览器中打开http://127.0.0.1:8000/,会看到成功运行站点的页面,如下图所示:

Django实战-博客项目_Django博客

能看到这个页面,说明Django正在运行,如果此时看一下刚才启动站点的命令行窗口,可以看到浏览器的GET请求:

[15/May/2018 17:20:30] "GET / HTTP/1.1" 200 16348

站点接受的每一个HTTP请求,都会显示在命令行窗口中,如果站点发生错误,也会将错误显示在该窗口中。

在启动站点的时候,还可以指定具体的主机地址和端口,或者使用另外一个配置文件,例如:

python manage.py runserver 127.0.0.1:8001 --settings=mysite.settings

如果站点需要在不同环境下运行,单独为每个环境创建匹配的配置文件。

当前这个站点只能用作开发测试,不能够配置为生产用途。想要将Django配置到生产环境中,必须通过一个Web服务程序比如Apache,Gunicorn或者uWSGI,将Django作为一个WSGI程序运行。使用不同web服务程序部署Django请参考:https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/。本书的第十三章 上线会介绍如何配置生产环境。

项目设置

打开settings.py看一下项目设置,其中列出了一些设置,但这只是Django所有设置的一部分。可以在https://docs.djangoproject.com/en/2.0/ref/settings/查看所有的设置和初始值。

文件中的以下设置值得注意:

  • DEBUG是一个布尔值,控制DEBUG模式的开启或关闭。当设置为True时,Django会将所有的日志和错误信息都打印在窗口中。在生产环境中则必须设置为False,否则会导致信息泄露。
  • ALLOWED_HOSTS在本地开发的时候,无需设置。在生产环境中,DEBUG设置为False时,必须将主机名/IP地址填入该列表中,以让Django为该主机/IP提供服务。
  • INSTALLED_APPS列出了每个项目当前激活的应用,Django默认包含下列应用:
    • django.contrib.admin:管理后台应用
    • django.contrib.auth:用户身份认证
    • django.contrib.contenttypes:追踪ORM模型与应用的对应关系
    • django.contrib.sessions:session应用
    • django.contrib.messages:消息应用
    • django.contrib.staticfiles:管理站点静态文件
  • MIDDLEWARE是中间件列表。
  • ROOT_URLCONF指定项目的根URL patterns配置文件。
  • DATABASE是一个字典,包含不同名称的数据库及其具体设置,必须始终有一个名称为default的数据库,默认使用SQLite 3数据库。
  • LANGUAGE_CODE站点默认的语言代码。
  • USE_TZ是否启用时区支持,Django可以支持根据时区自动切换时间显示。如果通过startproject命令创建站点,该项默认被设置为True

如果目前对这些设置不太理解也没有关系,在之后的章节中这里的设置都会使用到。

项目(projects)与应用(applications)

在整本书中,这两个词会反复出现。在Django中,像我们刚才那样的一套目录结构和其中的设置就是一个Django可识别的项目。应用指的就是一组Model(数据模型)、Views(视图)、Templates(模板)和URLs的集合。Django框架通过使用应用,为站点提供各种功能,应用还可以被复用在不同的项目中。你可以将一个项目理解为一个站点,站点中包含很多功能,比如博客,wiki,论坛,每一种功能都可以看作是一个应用。

创建一个应用

我们将从头开始创建一个博客应用,进入项目根目录(manage.py文件所在的路径),在系统命令行中输入以下命令创建第一个Django应用:

python manage.py startapp blog

这条命令会在项目根目录下创建一个如下结构的应用:

blog/
  __init__.py
  admin.py
  apps.py
  migrations/
    __init__.py
    models.py
    tests.py
    views.py

这些文件的含义为:

  • admin.py:用于将模型注册到管理后台,以便在Django的管理后台(Django administration site)查看。管理后台也是一个可选的应用。
  • apps.py:当前应用的主要配置文件
  • migrations这个目录包含应用的数据迁移记录,用来追踪数据模型的变化然后和数据库同步。
  • models.py:当前应用的数据模型,所有的应用必须包含一个models.py文件,但其中内容可以是空白。
  • test.py:为应用增加测试代码的文件
  • views.py:应用的业务逻辑部分,每一个视图接受一个HTTP请求,处理这个请求然后返回一个HTTP响应。

设计博客应用的数据架构(data schema)

schema是一个数据库名词,一般指的是数据在数据库中的组织模式或者说架构。我们将通过在Django中定义数据模型来设计我们博客应用在数据库中的数据架构。一个数据模型,是指一个继承了django.db.models.Model的Python 类。Django会为在models.py文件中定义的每一个类,在数据库中创建对应的数据表。Django为创建和操作数据模型提供了一系列便捷的API(Django ORM):

我们首先来定义一个Post类,在blog应用下的models.py文件中添加下列代码:

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

class Post(models.Model):
    STATUS_CHOICES = (('draft', 'Draft'), ('published', 'Published'))
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250, unique_for_date='publish')
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now())
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')

    class Meta:
        ordering = ('-publish',)

    def __str__(self):
        return self.title

这是我们为了博客中每一篇文章定义的数据模型:

  • title:这是文章标题字段。这个字段被设置为Charfield类型,在SQL数据库中对应VARCHAR数据类型
  • slug:该字段通常在URL中使用。slug是一个短的字符串,只能包含字母,数字,下划线和减号。将使用slug字段构成优美的URL,也方便搜索引擎搜索。其中的unique_for_date参数表示不允许两条记录的publish字段日期和title字段全都相同,这样就可以使用文章发布的日期与slug字段共同生成一个唯一的URL标识该文章。
  • author:是一个外键字段。通过这个外键,告诉Django一篇文章只有一个作者,一个作者可以写多篇文章。对于这个字段,Django会在数据库中使用外键关联到相关数据表的主键上。在这个例子中,这个外键关联到Django内置用户验证模块的User数据模型上。on_delete参数表示删除外键关联的内容时候的操作,这个并不是Django特有的定义,而是SQL 数据库的标准操作;将其设置为CASCADE意味着如果删除一个作者,将自动删除所有与这个作者关联的文章,对于该参数的设置,可以查看https://docs.djangoproject.com/en/2.0/ref/models/fields/#django.db.models.ForeignKey.on_deleterelated_name参数设置了从UserPost的反向关联关系,用blog_posts为这个反向关联关系命名,稍后会学习到该关系的使用。
  • body:是文章的正文部分。这个字段是一个文本域,对应SQL数据库的TEXT数据类型。
  • publish:文章发布的时间。使用了django.utils.timezone.now作为默认值,这是一个包含时区的时间对象,可以将其认为是带有时区功能的Python标准库中的datetime.now方法。
  • created:表示创建该文章的时间。auto_now_add表示当创建一行数据的时候,自动用创建数据的时间填充。
  • updated:表示文章最后一次修改的时间,auto_now表示每次更新数据的时候,都会用当前的时间填充该字段。
  • statues:这个字段表示该文章的状态,使用了一个choices参数,所以这个字段的值只能为一系列选项中的值。

Django提供了很多不同类型的字段可以用于数据模型,具体可以参考:https://docs.djangoproject.com/en/2.0/ref/models/fields/

在数据模型中的Meta类表示存放模型的元数据。通过定义ordering = ('-publish',),指定了Django在进行数据库查询的时候,默认按照发布时间的逆序将查询结果排序。逆序通过加在字段名前的减号表示。这样最近发布的文章就会排在前边。

__str__()方法是Python类的功能,供显示给人阅读的信息,这里将其设置为文章的标题。Django在很多地方比如管理后台中都调用该方法显示对象信息。

如果你之前使用的是Python 2.X,注意在Python 3中,所有的字符串都已经是原生Unicode格式,所以只需要定义__str__()方法,__unicode__()方法已被废弃。

激活应用

为了让Django可以为应用中的数据模型创建数据表并追踪数据模型的变化,必须在项目里激活应用。要激活应用,编辑settings.py文件,添加blog.apps.BlogConfigINSTALLED_APPS设置中:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog.apps.BlogConfig',
]

BlogConfig类是我们应用的配置类。现在Django就已经知道项目中包含了一个新应用,可以载入这个应用的数据模型了。

创建和执行迁移

创建好了博客文章的数据模型,之后需要将其变成数据库中的数据表。Django提供数据迁移系统,用于追踪数据模型的变动,然后将变化写入到数据库中。我们之前执行过的migrate命令会对INSTALLED_APPS中的所有应用进行扫描,根据数据模型和已经存在的迁移数据执行数据库同步操作。

首先,我们需要来为Post模型创建迁移数据,进入项目根目录,输入下列命令:

python manage.py makemigrations blog

会看到如下输出:

Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Post

该命令执行后会在blog应用下的migrations目录里新增一个0001_initial.py文件,可以打开该文件看一下迁移数据是什么样子的。一个迁移数据文件里包含了与其他迁移数据的依赖关系,以及实际要对数据库执行的操作。

为了了解Django实际执行的SQL语句,可以使用sqlmigrate加上迁移文件名,会列出要执行的SQL语句,但不会实际执行。在命令行中输入下列命令然后观察数据迁移的指令:

python manage.py sqlmigrate blog 0001

输出应该如下所示:

BEGIN;
--
-- Create model Post
--
CREATE TABLE "blog_post" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" varchar(250) NOT NULL, "slug" varchar(250) NOT NULL, "body" text NOT
NULL, "publish" datetime NOT NULL, "created" datetime NOT NULL, "updated"
datetime NOT NULL, "status" varchar(10) NOT NULL, "author_id" integer NOT
NULL REFERENCES "auth_user" ("id"));
CREATE INDEX "blog_post_slug_b95473f2" ON "blog_post" ("slug");
CREATE INDEX "blog_post_author_id_dd7a8485" ON "blog_post" ("author_id");
COMMIT;

具体的输出根据你使用的数据库会有变化。上边的输出针对SQLite数据库。可以看到表名被设置为应用名加上小写的类名(blog_post)也可以通过在Meta类中使用db_table属性设置表名。Django自动为每个模型创建了主键,也可以通过设置某个模型字段参数primary_key=True来指定主键。默认的主键列名叫做id,和这个列同名的id字段会自动添加到你的数据模型上。(即Post类被Django添加了Post.id属性)。

然后来让数据库与新的数据模型进行同步,在命令行中输入下列命令:

python manage.py migrate

会看到如下输出:

Applying blog.0001_initial... OK

这样就对INSTALLED_APPS中的所有应用执行完了数据迁移过程,包括我们的blog应用。在执行完迁移之后,数据库中的数据表就反映了我们此时的数据模型。

如果之后又编辑了models.py文件,对已经存在的数据模型进行了增删改,或者又添加了新的数据模型,必须重新执行makemigrations创建新的数据迁移文件然后执行migrate命令同步数据库。

为数据模型创建管理后台站点(administration site)

定义了Post数据模型之后,可以为方便的管理其中的数据创建一个简单的管理后台。Django内置了一个管理后台,这个管理后台动态的读入数据模型,然后创建一个完备的管理界面,从而可以方便的管理数据。这是一个可以“拿来就用”的方便工具。

管理后台功能其实也是一个应用叫做django.contrib.admin,默认包含在INSTALLED_APPS设置中。

创建超级用户

要使用管理后台,需要先注册一个超级用户,输入下列命令:

python manage.py createsuperuser

会看到下列输出,输入用户名、密码和邮件:

Username (leave blank to use 'admin'): admin
Email address: admin@admin.com
Password: 
Password (again): 
Superuser created successfully.

Django 管理后台

使用python manage.py runserver启动站点,然后打开http://127.0.0.1:8000/admin/,可以看到如下的管理后台登录页面:

Django实战-博客项目_Django实战_02

输入刚才创建的超级用户的用户名和密码,可以看到管理后台首页,如下所示:

Django实战-博客项目_Django博客_03

GroupUser已经存在于管理后台中,这是因为设置中默认启用了django.contrib.auth应用的原因。如果你点击Users,可以看到刚刚创建的超级用户。还记得blog应用的Post模型与User模型通过author字段产生外键关联吗?

向管理后台内添加模型

我们把Post文章模型添加到管理后台中,编辑blog应用的admin.py文件为如下这样:

from django.contrib import admin
from .models import Post

admin.site.register(Post)

之后刷新管理后台页面,可以看到Post类出现在管理后台中:

Django实战-博客项目_Django博客_04

看上去好像很简单。每当在管理后台中注册一个模型,就能迅速在管理后台中看到它,还可以对其进行增删改查。

点击Posts右侧的Add链接,可以看到Django根据模型的具体字段动态的生成了添加页面,如下所示:

Django实战-博客项目_Django实战_05

Django对于每个字段使用不同的表单插件(form widgets,控制该字段实际在页面上对应的HTML元素)。即使是比较复杂的字段比如DateTimeField,也会以简单的界面显示出来,类似于一个JavaScript的时间控件。

填写完这个表单然后点击SAVE按钮,被重定向到文章列表页然后显示一条成功信息,像下面这样:

Django实战-博客项目_Django博客_06

可以再录入一些文章数据,为之后数据库相关操作做准备。

自定义模型在管理后台的显示

现在我们来看一下如何自定义管理后台,编辑blog应用的admin.py,修改成如下:

from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status')

这段代码的意思是将我们的模型注册到管理后台中,并且创建了一个类继承admin.ModelAdmin用于自定义模型的展示方式和行为。list_display属性指定那些字段在详情页中显示出来。@admin.register()装饰器的功能与之前的admin.site.register()一样,用于将PostAdmin类注册成Post的管理类。

再继续添加一些自定义设置,如下所示:

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status',)
    list_filter = ('status', 'created', 'publish', 'author',)
    search_fields = ('title', 'body',)
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ('author',)
    date_hierarchy = 'publish'
    ordering = ('status', 'publish',)

回到浏览器,刷新一Posts文章的列表页,会看到如下所示:

Django实战-博客项目_Django博客_07

可以看到在该页面上显示的字段就是list_display中的字段。页面出现了一个右侧边栏用于筛选结果,这个功能由list_filter属性控制。页面上方出现了一个搜索栏,这是因为在search_fields中定义了可搜索的字段。在搜索栏的下方,出现了时间层级导航条,这是在date_hierarchy中定义的。还可以看到文章默认通过Status和Publish字段进行排序,这是由ordering属性设置的。

这个时候点击Add Post,可以发现也有变化。当输入文章标题时,slug字段会根据标题自动填充,这是因为设置了prepopulated_fields属性中slug字段与title字段的对应关系。现在author字段旁边出现了一个搜索图标,并且可以按照ID来查找和显示作者,如果在用户数量很大的时候,这就方便太多了。

通过短短几行代码,就可以自定义模型在管理后台中的显示方法,还有很多自定义管理后台和扩展管理后台功能的方法,会在以后的各章中逐步遇到。

使用QuerySet和模型管理器(managers)

现在我们有了一个功能齐备的管理后台用于管理博客的内容数据,现在可以来学习如何从数据库中查询数据并且对结果进行操作了。Django具有一套强大的API,可以供你轻松的实现增删改查的功能,这就是Django Object-relational-mapper即Django ORM,可以兼容MySQL,PostgreSQL,SQLite和Oracle,可以在settings.pyDATABASES中修改数据库设置。可以通过编辑数据库的路由设置让Django同时使用多个数据库。

一旦你创建好了数据模型,Django就提供了一套API供你操作数据模型,详情可以参考https://docs.djangoproject.com/en/2.0/ref/models/

创建数据对象

打开系统的终端窗口,运行如下命令:

python manage.py shell

然后录入如下命令:

from django.contrib.auth.models import User
>>>from blog.models import Post
>>>user = User.objects.get(username='admin')
>>>post = Post(title='Another post', slug='another-post', body='Post body', author = user)
>>>post.save()

让我们来分析一下这段代码做的事情:我们先通过用户名admin取得user对象,就是下边这条命令:

user = User.objects.get(username='admin')

get()方法允许从数据库中取出单独一个数据对象。如果找不到对应数据,会抛出DoseNotExist异常,如果结果超过一个,会抛出MultipleObjectsReturn异常,这两个异常都是被查找的类的属性。

然后我们通过下边这条命令,使用了标题,简称和文章内容,以及指定author字段为刚取得的User对象,新建了一个Post对象:

post = Post(title='Another post', slug='another-post', body='Post body', author = user)

这个对象暂时保存在内存中,没有被持久化(写入)到数据库中。

最后,我们通过save()方法将Post对象写入到数据库中:

post.save()

这条命令实际会转化成一条INSERT SQL语句。现在我们已经知道了如何在内存中先创建一个数据对象然后将其写入到数据库中的方法,我们还可以使用create()方法一次性创建并写入数据库,像这样:

Post.objects.create(title='One more post', slug='One more post', body='Post body', author=user)

修改数据对象

现在,修改刚才的post对象的标题:

 post.title = 'New title'
>>> post.save()

这次save()方法实际转化为一个UPDATESQL语句。

对数据对象做的修改直到调用save()方法才会被存入数据库。

查询数据

Django ORM的全部使用都基于QuerySet(查询结果集对象,由于该术语使用频繁,因此在之后的文章中不再进行翻译)。一个查询结果集是一系列从数据库中取得的数据对象,经过一系列的过滤条件,最终组合到一起构成的一个对象。

之前已经了解了使用Post.objects.get()方法从数据库中取出一个单独的数据对象,每个模型都有至少一个管理器,默认的管理器叫做objects。通过使用一个模型管理器,可以得到一个QuerySet,想得到一个数据表里的所有数据对象,可以使用默认模型管理器的all()方法,像这样:

 all_posts = Post.objects.all()

这样就取得了一个包含数据库中全部post的Queryset,值得注意的是,QuerySet还没有被执行(即执行SQL语句),因为QuerySet是惰性求值的,只有在确实要对其进行表达式求值的时候,QuerySet才会被执行。惰性求值特性使得QuerySet非常有用。如果我们不是把QuerySet的结果赋值给一个变量,而是直接写在Python命令行中,对应的SQL语句就会立刻被执行,因为会强制对其求值:

 Post.objects.all()

译者注:原书一直没有非常明确的指出这几个概念,估计是因为本书不是面向Django初学者所致。这里译者总结一下:数据模型Model类=数据表,数据模型类的实例=数据表的一行数据(不一定是来自于数据库的,也可能是内存中创建的),查询结果集=包装一系列数据模型类实例的对象。

使用filter()方法

可以使用模型管理器的filter()过滤所需的数据,例如,可以过滤出所有2017年发布的博客文章:

Post.objects.filter(publish__year=2017)

还可以同时使用多个字段过滤,比如选出所有admin作者于2017年发布的文章:

Post.objects.filter(publish__year=2017, author__username='admin')

这和链式调用QuerySet的结果一样:

Post.objects.filter(publish__year=2017).filter(author__username='admin')

QuerySet中使用的条件查询采用双下划线写法,比如例子中的publish__year,双下划线还一个用法是从关联的模型中取其字段,例如author__username

使用exclude()方法

使用模型管理器的exclude()从结果集中去除符合条件的数据。例如选出2017年发布的所有标题不以Why开头的文章:

Post.objects.filter(publish__year=2017).exclude(title__startswith='Why')

使用order_by()方法

对于查询出的结果,可以使用order_by()方法按照不同的字段进行排序。例如选出所有文章,使其按照title字段排序:

Post.objects.order_by('title')

默认会采用升序排列,如果需要使用降序排列,在字符串格式的字段名前加一个减号:

Post.objects.order_by('-title')

译者注:如果不指定order_by的排序方式,但在Meta中指定了顺序,则默认会优先以Meta中的顺序列出。

删除数据

如果想删除一个数据,可以对一个数据对象直接调用delete()方法:

post = Post.objects.get(id=1)
post.delete()

当外键中的on_delete参数被设置为CASCADE时,删除一个对象会同时删除所有对其有依赖关系的对象,比如删除作者的时候该作者的文章会一并删除。

译者注:filter()exclude()all()这三个方法都返回一个QuerySet对象,所以可以任意链式调用。

QuerySet何时会被求值

可以对一个QuerySet串联任意多的过滤方法,但只有到该QuerySet实际被求值的时候,才会进行数据库查询。QuerySet仅在下列时候才被实际执行:

  • 第一次迭代QuerySet
  • 执行切片操作,例如Post.objects.all()[:3]
  • pickled或者缓存QuerySet的时候
  • 调用QuerySet的repr()或者len()方法
  • 显式对其调用list()方法将其转换成列表
  • 将其用在逻辑判断表达式中。比如bool()orandif

如果对结构化程序设计中的表达式求值有所了解的话,就可以知道只有表达式被实际求值的时候,QuerySet才会被执行。译者在这里推荐伯克利大学的CS 61A: Structure and Interpretation of Computer ProgramsPython教程。

创建模型管理器

像之前提到的那样,类名后的.objects就是默认的模型管理器,所有的ORM方法都通过模型管理器操作。除了默认的管理器之外,我们还可以自定义这个管理器。我们要创建一个管理器,用于获取所有status字段是published的文章。

自行编写模型管理器有两种方法:一是给默认的管理器增加新的方法,二是修改默认的管理器。第一种方法就像是给你提供了一个新的方法例如:Post.objects.my_manager(),第二种方法则是直接使用新的管理器例如:Post.my_manager.all()。我们想实现的方式是:Post.published.all()这样的管理器。

blogmodels.py里增加自定义的管理器:

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super(PublishedManager, self).get_queryset().filter(status='published')

class Post(models.Model):
    # ......
    objects = models.Manager()  # 默认的管理器
    published = PublishedManager()  # 自定义管理器

模型管理器的get_queryset()方法返回后续方法要操作的QuerySet,我们重写了该方法,以让其返回所有过滤后的结果。现在我们已经自定义好了管理器并且将其添加到了Post模型中,现在可以使用这个管理器进行数据查询,来测试一下:

启动包含Django环境的Python命令行模式:

python manage.py shell

现在可以取得所有标题开头是Who,而且已经发布的文章(实际的查询结果根据具体数据而变):

Post.published.filter(title__startswith="Who")

创建列表和详情视图函数

在了解了ORM的相关知识以后,就可以来创建视图了。视图是一个Python中的函数,接受一个HTTP请求作为参数,返回一个HTTP响应。所有返回HTTP响应的业务逻辑都在视图中完成。

首先,我们会创建应用中的视图,然后会为每个视图定义一个匹配的URL路径,最后,会创建HTML模板将视图生成的结果展示出来。每一个视图都会向模板传递参数并且渲染模板,然后返回一个包含最终渲染结果的HTTP响应。

创建视图函数

来创建一个视图用于列出所有的文章。编辑blog应用的views.py文件:

from django.shortcuts import render, get_object_or_404
from .models import Post

def post_list(request):
    posts = Post.published.all()
    return render(request, 'blog/post/list.html', {'posts': posts})

我们创建了第一个视图函数--文章列表视图。post_list目前只有一个参数request,这个参数对于所有的视图都是必需的。在这个视图中,取得了所有已经发布(使用了published管理器)的文章。

最后,使用由django.shortcuts提供的render()方法,使用一个HTML模板渲染结果。render()方法的参数分别是reqeust,HTML模板的位置,传给模板的变量名与值。render()方法返回一个带有渲染结果(HTML文本)的HttpResponse对象。render()方法还会将request对象携带的变量也传给模板,在模板中可以访问所有模板上下文管理器设置的变量。模板上下文管理器就是将变量设置到模板环境的可调用对象,会在第三章学习到。

再写一个显示单独一篇文章的视图,在views.py中添加下列函数:

def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, slug=post, status="published", publish__year=year, publish__month=month,
                             publish__day=day)
    return render(request, 'blog/post/detail.html', {'post': post})

这就是我们的文章详情视图。这个视图需要yearmonthdaypost参数,用于获取一个指定的日期和简称的文章。还记得之前创建模型时设置slug字段的unique_for_date参数,这样通过日期和简称可以找到唯一的一篇文章(或者找不到)。使用get_object_or_404()方法来获取文章,这个方法返回匹配的一个数据对象,或者在找不到的情况下返回一个HTTP 404错误(not found)。最后使用render()方法通过一个模板渲染页面。

为视图配置URL

URL pattern的作用是将URL映射到视图上。一个URL pattern由一个正则字符串,一个视图和可选的名称(该名称必须唯一,可以在整个项目环境中使用)组成。Django接到对于某个URL的请求时,按照顺序从上到下试图匹配URL,停在第一个匹配成功的URL处,将HttpRequest类的一个实例和其他参数传给对应的视图并调用视图处理本次请求。

blog应用下目录下边新建一个urls.py文件,然后添加如下内容:

from django.urls import path
from . import views

app_name = 'blog'
urlpatterns = [
    # post views
    path('', views.post_list, name='post_list'),
    path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.post_detail, name='post_detail'),
]

上边的代码中,通过app_name定义了一个命名空间,方便以应用为中心组织URL并且通过名称对应到URL上。然后使用path()设置了两条具体的URL pattern。第一条没有任何的参数,对应post_list视图。第二条需要如下四个参数并且对应到post_detail视图:

  • year:需要匹配一个整数
  • month:需要匹配一个整数
  • day:需要匹配一个整数
  • post:需要匹配一个slug形式的字符串

我们使用了一对尖括号从URL中获取这些参数。任何URL中匹配上这些内容的文本都会被捕捉为这个参数的对应的类型值。例如<int:year>会匹配到一个整数形式的字符串然后会给模板传递名称为int的变量,其值为捕捉到的字符串转换为整数后的值。而<slug:post>则会被转换成一个名称为post,值为slug类型(仅有ASCII字符或数字,减号,下划线组成的字符串)的变量传给视图。

对于URL匹配的类型,可以参考https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters

如果使用path()无法满足需求,则可以使用re_path(),通过Python正则表达式匹配复杂的URL。参考https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.re_path了解re_path()的使用方法,参考https://docs.python.org/3/howto/regex.html了解Python中如何使用正则表达式。

为每个视图创建单独的urls.py文件是保持应用可被其他项目重用的最好方式。

现在我们必须把blog应用的URL包含在整个项目的URL中,到mysite目录下编辑urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace='blog')),
]

这行新的URL使用include方法导入了blog应用的所有URL,使其位于blog/URL路径下,还指定了命名空间blog。URL命名空间必须在整个项目中唯一。之后我们方便的通过使用命名空间来快速指向具体的URL,例如blog:post_listblog:post_detail。关于URL命名空间可以参考https://docs.djangoproject.com/en/2.0/topics/http/urls/#url-namespaces

规范模型的URL

可以使用在上一节创建的post_detail URL来为Post模型的每一个数据对象创建规范化的URL。通常的做法是给模型添加一个get_absolute_url()方法,该方法返回对象的URL。我们将使用reverse()方法通过名称和其他参数来构建URL。编辑models.py文件

from django.urls import reverse

class Post(models.Model):
    # ......
    def get_absolute_url(self):
        return reverse('blog:post_detail', args=[self.publish.year, self.publish.month, self.publish.day, self.slug])

之后在模板中,就可以使用get_absolute_url()创建超链接到具体数据对象。

译者注:原书这里写得很简略,实际上反向解析URL是创建结构化站点非常重要的内容,可以参考Django 1.11版本的Django进阶-路由系统了解原理,Django 2.0此部分变化较大,需研读官方文档。

为视图创建模板

已经为blog应用配置好了URL pattern,现在需要将内容通过模板展示出来。

blog应用下创建如下目录:

templates/
    blog/
        base.html
        post/
            list.html
            detail.html

这就是模板的目录结构。base.html包含页面主要的HTML结构,并且将结构分为主体内容和侧边栏两部分。list.htmldetail.html会分表继承base.html并渲染各自的内容。

Django提供了强大的模板语言用于控制数据渲染,由模板标签(template tags)模板变量(template variables)模板过滤器(template filters)组成:

  • template tags:进行渲染控制,类似{% tag %}
  • template variables:可认为是模板标签的一种特殊形式,即只是一个变量,渲染的时候只替换内容,类似{{ variable }}
  • template filters:附加在模板变量上改变变量最终显示结果,类似{{ variable|filter }}

所有内置的模板标签和过滤器可以参考https://docs.djangoproject.com/en/2.0/ref/templates/builtins/

编辑base.html,添加下列内容:

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>{% block title %}{% endblock %}</title>
    <link rel="stylesheet" href="{% static "css/blog.css" %}">
</head>
<body>
    <div id="content">
        {% block content %}
        {% endblock %}
    </div>
    <div id="sidebar">
        <h2>My blog</h2>
        <p>This is my blog.</p>
    </div>
</body>
</html>

{% load static %} 表示导入由django.contrib.staticfiles应用提供的static模板标签,导入之后,在整个当前模板中都可以使用{% static %}标签从而导入静态文件例如blog.css(可在本书配套源码blog应用的static/目录下找到,将其拷贝到你的项目的相同位置)。

还可以看到有两个{% block %}表示这个标签的开始与结束部分定义了一个块,继承该模板的模板将用具体内容替换这两个块。这两个块的名称是titlecontent

编辑post/list.html

{% extends "blog/base.html" %}
{% block title %}My Blog{% endblock %}
{% block content %}
    <h1>My Blog</h1>
    {% for post in posts %}
        <h2>
            <a href="{{ post.get_absolute_url }}">
                {{ post.title }}
            </a>
        </h2>
        <p class="date">
        Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
{% endblock %}

通过使用{% extends %},让该模板继承了母版blog/base.html,然后用实际内容填充了titlecontent块。通过迭代所有的文章,展示文章标题,发布日期,作者、正文及一个链接到文章的规范化URL。在正文部分使用了两个filter:truncatewords用来截断指定数量的文字,linebreaks将结果带上一个HTML换行。filter可以任意连用,每个都在上一个的结果上生效。

打开系统命令行输入python manage.py runserver启动站点,然后在浏览器中访问http://127.0.0.1:8000/blog/,可以看到如下页面(如果没有文章,通过管理后台添加一些):

Django实战-博客项目_Django博客_08

然后编辑post/detail.html

{% extends 'blog/base.html' %}
{% block title %}
{{ post.title }}
{% endblock %}

{% block content %}
    <h1>{{ post.title }}</h1>
    <p class="date">
    Published {{ post.publish }} by {{ post.author }}
    </p>
    {{ post.body|linebreaks }}
{% endblock %}

现在可以回到刚才的页面,点击任何一篇文章可以看到详情页:

Django实战-博客项目_Django实战_09

看一下此时的URL,应该类似/blog/2017/12/14/who-was-djangoreinhardt/。这就是我们生成的规范化的URL。

添加分页功能

当输入一些文章后,你会很快意识到需要将所有的文章分页进行显示。Django自带了一个分页器可以方便地进行分页。

编辑blog应用的views.py文件,修改post_list视图:

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def post_list(request):
    object_list = Post.published.all()
    paginator = Paginator(object_list, 3)  # 每页显示3篇文章
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        # 如果page参数不是一个整数就返回第一页
        posts = paginator.page(1)
    except EmptyPage:
        # 如果页数超出总页数就返回最后一页
        posts = paginator.page(paginator.num_pages)
    return render(request, 'blog/post/list.html', {'page': page, 'posts': posts})

分页器相关代码解释如下:

  1. 使用要分页的内容和每页展示的内容数量,实例化Paginator类得到paginator对象
  2. 通过get()方法获取page变量,表示当前的页码
  3. 调用paginator.page()方法获取要展示的数据
  4. 如果page参数不是一个整数就返回第一页,如果页数超出总页数就返回最后一页
  5. 把页码和要展示的内容传给页面。

现在需要为分页功能创建一个单独的模板,以让该模板可以包含在任何使用分页功能的页面中,在blog应用的templates/目录中新建pagination.html,添加如下代码:


    <span class="step-links">
        {% if page.has_previous %}
        <a href="?page={{ page.previous_page_number }}">Previous</a>
        {% endif %}
    <span class="current">
        Page {{ page.number }} of {{ page.paginator.num_pages }}.
    </span>
    {% if page.has_next %}
        <a href="?page={{ page.next_page_number }}">Next</a>
    {% endif %}
    </span>
</div>

这个用于分页的模板接受一个名称为Page的对象,然后显示前一页,后一页和总页数。为此,回到blog/post/list.html文件,在{% content %}中的最下边增加一行:

{% block content %}
    # ......
    {% include 'pagination.html' with page=posts %}
{% endblock %}

由于视图传递给列表页的Page对象的名称叫做posts,所以通过with重新指定了变量名称以让分页模板也能正确接收到该对象。

打开浏览器到http://127.0.0.1:8000/blog/,可以看到页面如下:

Django实战-博客项目_Django博客_10

使用基于类的视图

Python中类可以取代函数,视图是一个接受HTTP请求并返回HTTP响应的可调用对象,所以基于函数的视图(FBV)也可以通过基于类的视图(CBV)来实现。Django为CBV提供了基类View,包含请求分发功能和其他一些基础功能。

CBV相比FBV有如下优点

  • 可编写单独的方法对应不同的HTTP请求类型如GET,POST,PUT等请求,不像FBV一样需要使用分支
  • 使用多继承创建可复用的类模块(也叫做mixins

可以看一下关于CBV的介绍:https://docs.djangoproject.com/en/2.0/topics/class-based-views/intro/

我们用Django的内置CBV类ListView来改写post_list视图,ListView的作用是列出任意类型的数据。编辑blog应用的views.py文件,添加下列代码:

from django.views.generic import ListView
class PostListView(ListView):
    queryset = Post.published.all()
    context_object_name = 'posts'
    paginate_by = 3
    template_name = 'blog/post/list.html'

这个CBV和post_list视图函数的功能类似,在上边的代码里做了以下工作:

  • 使用queryset变量查询所有已发布的文章。实际上,可以不使用这个变量,通过指定model = Post,这个CBV就会去进行Post.objects.all()查询获得全部文章。
  • 设置posts为模板变量的名称,如果不设置context_object_name参数,默认的变量名称是object_list
  • 设置paginate_by为每页显示3篇文章
  • 通过template_name指定需要渲染的模板,如果不指定,默认使用blog/post_list.html

打开blog应用的urls.py文件,注释掉刚才的post_list URL pattern,为PostListView类增加一行:

urlpatterns = [
    # post views
    # path('', views.post_list, name='post_list'),
    path('',views.PostListView.as_view(),name='post_list'),
    path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.post_detail, name='post_detail'),
]

为了正常使用分页功能,需要使用正确的变量名称,Django内置的ListView返回的变量名称叫做page_obj,所以必须修改post/list.html中导入分页模板的那行代码:

{% include 'pagination.html' with page=page_obj %}

在浏览器中打开http://127.0.0.1:8000/blog/,看一下是否和原来使用post_list的结果一样。这是一个简单的CBV示例,会在第十章更加深入的了解CBV的使用。

总结

这一章通过创建一个简单的博客应用,学习了基础的Django框架使用方法:设计了数据模型并且进行了数据模型迁移,创建了视图,模板和URLs,还学习了分页功能。下一章将学习给博客增加评论系统和标签分类功能,以及通过邮件分享文章链接的功能。

增强博客功能

在之前的章节创建基础的博客应用,现在可以变成具备通过邮件分享文章,带有评论和标签系统等功能的完整博客。在这一章会学习到如下内容:

  • 使用Django发送邮件
  • 创建表单并且通过视图控制表单
  • 通过模型生成表单
  • 集成第三方应用
  • 更复杂的ORM查询

通过邮件分享文章

首先来制作允许用户通过邮件分享文章链接的功能。在开始之前,先想一想你将如何使用视图、URLs和模板来实现这个功能,然后看一看你需要做哪些事情:

  • 创建一个表单供用户填写名称和电子邮件地址收件人,以及评论等。
  • views.py中创建一个视图控制这个表单,处理接收到的数据然后发送电子邮件
  • 为新的视图在urls.py中配置URL
  • 创建展示表单的模板

使用Django创建表单

Django内置一个表单框架,可以简单快速的创建表单。表单框架具有自定义表单字段、确定实际显示的方式和验证数据的功能。

Django使用两个类创建表单:

  • Form:用于生成标准的表单
  • ModelForm:用于从模型生成表单

blog应用中创建一个forms.py文件,然后编写:

from django import forms

class EmailPostForm(forms.Form):
    name = forms.CharField(max_length=25)
    email = forms.EmailField()
    to = forms.EmailField()
    comments = forms.CharField(required=False, widget=forms.Textarea)

这是使用forms类创建的第一个标准表单,通过继承内置Form类,然后设置字段为各种类型,用于验证数据。

表单可以编写在项目的任何位置,但通常将其编写在对应应用的forms.py文件中。

name字段是Charfield类型,会被渲染为<input type="text">HTML标签。每个字段都有一个默认的widget参数决定该字段被渲染成的HTML元素类型,可以通过widget参数改写。在comments字段中,使用了widget=forms.Textarea令该字段被渲染为一个<textarea>元素,而不是默认的<input>元素。

字段验证也依赖于字段属性。例如:emailto字段都是EmailField类型,两个字段都接受一个有效的电子邮件格式的字符串,否则这两个字段会抛出forms.ValidationError错误。表单里还存在的验证是:name字段的最大长度maxlength是25个字符,comments字段的required=False表示该字段可以没有任何值。所有的这些设置都会影响到表单验证。本表单只使用了很少一部分的字段类型,关于所有表单字段可以参考https://docs.djangoproject.com/en/2.0/ref/forms/fields/

通过视图控制表单

现在需要写一个视图,用于处理表单提交来的数据,当表单成功提交的时候发送电子邮件。编辑blog应用的views.py文件:

from .forms import EmailPostForm

def post_share(request, post_id):
    # 通过id 获取 post 对象
    post = get_object_or_404(Post, id=post_id, status='published')
    if request.method == "POST":
        # 表单被提交
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # 验证表单数据
            cd = form.cleaned_data
            # 发送邮件......
    else:
        form = EmailPostForm()
    return render(request, 'blog/post/share.html', {'post': post, 'form': form})

这段代码的逻辑如下:

  • 定义了post_share视图,参数是request对象和post_id
  • 使用get_object_or_404()方法,通过ID和published取得所有已经发布的文章中对应ID的文章。
  • 这个视图同时用于显示空白表单和处理提交的表单数据。我们先通过request.method判断当前请求是POST还是GET请求。如果是GET请求,展示一个空白表单;如果是POST请求,需要处理表单数据。

处理表单数据的过程如下:

  1. 视图收到GET请求,通过form = EmailPostForm()创建一个空白的form对象,展示在页面中是一个空白的表单供用户填写。
  2. 用户填写并通过POST请求提交表单,视图使用request.POST中包含的表单数据创建一个表单对象:
if request.method == 'POST':
    # 表单被提交
    form = EmailPostForm(request.POST)
  1. 在上一步之后,调用表单对象的is_valid()方法。这个方法会验证表单中所有的数据是否有效,如果全部通过验证会返回True,任意一个字段未通过验证,is_valid()就会返回False。如果返回False,此时可以在form.errors属性中查看错误信息。
  2. 如果表单验证失败,我们将这个表单对象渲染回页面,页面中会显示错误信息。
  3. 如果表单验证成功,可以通过form.cleaned_data属性访问表单内所有通过验证的数据,这个属性类似于一个字典,包含字段名与值构成的键值对。

如果表单验证失败,form.cleaned_data只会包含通过验证的数据。

现在就可以来学习如何使用Django发送邮件了。

使用Django发送邮件

使用Django发送邮件比较简单,需要一个本地或者外部的SMTP服务器,然后在settings.py文件中加入如下设置:

  • EMAIL_HOST:邮件主机,默认是localhost
  • EMAIL_PORT:SMTP服务端口,默认是25
  • EMAIL_HOST_USER:SMTP服务器的用户名
  • EMAIL_HOST_PASSWORD:SMTP服务器的密码
  • EMAIL_USE_TLS:是否使用TLS进行连接
  • EMAIL_USE_SSL:是否使用SSL进行连接

如果无法使用任何SMTP服务器,则可以将邮件打印在命令行窗口中,在settings.py中加入下列这行:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

这样会把所有的邮件内容显示在控制台,非常便于测试。

如果没有本地SMTP服务器,可以使用很多邮件服务供应商提供的SMTP服务,以下是使用Google的邮件服务示例:

EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True

输入python manage.py shell,在命令行环境中试验一下发送邮件的指令:

from django.core.mail import send_mail
send_mail('Django mail', 'This e-mail was sent with Django.', 'your_account@gmail.com', ['your_account@gmail.com'], fail_silently=False)

send_mail()方法的参数分别是邮件标题、邮件内容、发件人和收件人地址列表,最后一个参数fail_silently=False表示如果发送失败就抛出异常。如果看到返回1,就说明邮件成功发送。

如果采用以上设置无法成功使用Google的邮件服务,需要到https://myaccount.google.com/lesssecureapps,启用“允许不够安全的应用”,如下图所示:

Django实战-博客项目_Django实战_11

现在我们把发送邮件的功能加入到视图中,编辑views.py中的post_share视图函数:

def post_share(request, post_id):
    # 通过id 获取 post 对象
    post = get_object_or_404(Post, id=post_id, status='published')
    sent = False

    if request.method == "POST":
        # 表单被提交
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # 表单字段通过验证
            cd = form.cleaned_data
            post_url = request.build_absolute_uri(post.get_absolute_url())
            subject = '{} ({}) recommends you reading "{}"'.format(cd['name'], cd['email'], post.title)
            message = 'Read "{}" at {}\n\n{}\'s comments:{}'.format(post.title, post_url, cd['name'], cd['comments'])
            send_mail(subject, message, 'lee0709@vip.sina.com', [cd['to']])
            sent = True

    else:
        form = EmailPostForm()
    return render(request, 'blog/post/share.html', {'post': post, 'form': form, 'sent': sent})

声明了一个sent变量用于向模板返回邮件发送的状态,当邮件发送成功的时候设置为True。稍后将使用该变量显示一条成功发送邮件的消息。由于要在邮件中包含连接,因此使用了get_absolute_url()方法获取被分享文章的URL,然后将其作为request.build_absolute_uri()的参数转为完整的URL,再加上表单数据创建邮件正文,最后将邮件发送给to字段中的收件人。

还需要给视图配置URL,打开blog应用中的urls.py,加一条post_share的URL pattern:

urlpatterns = [
    # ...
    path('<int:post_id>/share/', views.post_share, name='post_share'),
]

在模板中渲染表单

在创建表单,视图和配置好URL之后,现在只剩下模板了。在blog/templates/blog/post/目录内创建share.html,添加如下代码:

{% extends "blog/base.html" %}

{% block title %}Share a post{% endblock %}

{% block content %}
    {% if sent %}
        <h1>E-mail successfully sent</h1>
        <p>
            "{{ post.title }}" was successfully sent to {{ form.cleaned_data.to }}.
        </p>
    {% else %}
        <h1>Share "{{ post.title }}" by e-mail</h1>
        <form action="." method="post">
        {{ form.as_p }}
            {% csrf_token %}
            <input type="submit" value="Send e-mail">
        </form>
    {% endif %}
{% endblock %}

这个模板在邮件发送成功的时候显示一条成功信息,否则则显示表单。你可能注意到了,创建了一个HTML表单元素并且指定其通过POST请求提交:

<form action="." method="post">

之后渲染表单实例,通过使用as_p方法,将表单中的所有元素以

元素的方式展现出来。还可以使用as_ulas_table分别以列表和表格的形式显示。如果想分别渲染每个表单元素,可以迭代表单对象中的每个元素,例如这样:

{% for field in form %}
    <div>
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
    </div>
{% endfor %}

{% csrf_token %}在页面中显示为一个隐藏的input元素,是一个自动生成的防止跨站请求伪造(CSRF)攻击的token。跨站请求伪造是一种冒充用户在已经登录的Web网站上执行非用户本意操作的一种攻击方式,可能由其他网站或一段程序发起。关于CRSF的更多信息可以参考https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

例子生成的隐藏字段类似如下:

<input type='hidden' name='csrfmiddlewaretoken' value='26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR' />

Django默认会对所有POST请求进行CSRF检查,在所有POST方式提交的表单中,都要添加csrf_token

修改blog/post/detail.html将下列链接增加到{{ post.body|linebreaks }}之后:

<p>
    <a href="{% url "blog:post_share" post.id %}">Share this post</a>
</p>

这里的{% url %}标签,其功能和在视图中使用的reverse()方法类似,使用URL的命名空间blog和URL命名post_share,再传入一个ID作为参数,就可以构建出一个URL。在页面渲染时,{% url %}就会被渲染成反向解析出的URL。

现在使用python manage.py runserver启动站点,打开http://127.0.0.1:8000/blog/,点击任意文章查看详情页,在文章的正文下会出现分享链接,如下所示:

Django实战-博客项目_Django博客_12

点击 Share this post 链接,可以看到分享页面:

Django实战-博客项目_Django博客_13

这个表单的CSS样式表文件位于static/css/blog.css。当你点击SEND E-MAIL按钮的时候,就会提交表单并验证数据,如果有错误,可以看到页面如下:

Django实战-博客项目_Django实战_14

在某些现代浏览器上,很有可能浏览器会阻止你提交表单,提示必须完成某些字段,这是因为浏览器在提交根据表单的HTML元素属性先进行了验证。现在通过邮件分享链接的功能制作完成了,下一步是创建一个评论系统。

关闭浏览器验证的方法是给表单添加novalidate属性:<form action="" novalidate>

创建评论系统

现在,我们要创建一个评论系统,让用户可以对文章发表评论。创建评论系统需要进行以下步骤:

  1. 创建一个模型用于存储评论
  2. 创建一个表单用于提交评论和验证数据
  3. 创建一个视图用于处理表单和将表单数据存入数据库
  4. 编辑文章详情页以展示评论和提供增加新评论的表单

首先来创建评论对应的数据模型,编辑blog应用的models.py文件,添加以下代码:

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    name = models.CharField(max_length=80)
    email = models.EmailField()
    body = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    active = models.BooleanField(default=True)

    class Meta:
        ordering = ("created",)

    def __str__(self):
        return 'Comment by {} on {}'.format(self.name, self.post)

Comment模型包含一个外键(ForeignKey)用于将评论与一个文章联系起来,定义了文章和评论的一对多关系,即一个文章下边可以有多个评论;外键的related_name参数定义了在通过文章查找其评论的时候引用该关联关系的名称。这样定义了该外键之后,可以通过comment.post获得一条评论对应的文章,通过post.comments.all()获得一个文章对应的所有评论。如果不定义related_name,Django会使用模型的小写名加上_setcomment_set)来作为反向查询的管理器名称。

关于一对多关系的可以参考https://docs.djangoproject.com/en/2.0/topics/db/examples/many_to_one/

模型还包括一个active布尔类型字段,用于手工关闭不恰当的评论;还指定了排序方式为按照created字段进行排序。

新的Comment模型还没有与数据库同步,执行以下命令创建迁移文件:

python manage.py makemigrations blog

会看到如下输出:

Migrations for 'blog':
  blog/migrations/0002_comment.py
    - Create model Comment

Django在migrations/目录下创建了0002_comment.py文件,现在可以执行实际的迁移命令将模型写入数据库:

python manage.py migrate

会看到以下输出:

Applying blog.0002_comment... OK

数据迁移的过程结束了,数据库中新创建了名为blog_comment的数据表。

译者注:数据迁移的部分在原书中重复次数太多,而且大部分无实际意义,如无需要特殊说明的地方,以下翻译将略过类似的部分,以“执行数据迁移”或类似含义的字样替代。

创建了模型之后,可以将其加入管理后台。打开blog应用中的admin.py文件,导入Comment模型,然后增加如下代码:

from .models import Post, Comment

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ('name', 'email', 'post', 'created', 'active')
    list_filter = ('active', 'created', 'updated')
    search_fields = ('name', 'email', 'body')

启动站点,到http://127.0.0.1:8000/admin/查看管理站点,会看到新的模型已经被加入到管理后台中:

Django实战-博客项目_Django博客_15

根据模型创建表单

在发送邮件的功能里,采用继承forms.Form类的方式,自行编写各个字段创建了一个表单。Django对于表单有两个类:FormModelForm。这次我们使用ModelForm动态的根据Comment模型生成表单。编辑blog应用的forms.py文件:

from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('name', 'email', 'body')

依据模型创建表单,只需要在Meta类中指定基于哪个类即可。Django会自动内省该类然后创建对应的表单。我们对于模型字段的设置会影响到表单数据的验证规则。默认情况下,Django对每一个模型字段都创建一个对应的表单元素。然而,可以显示的通过Meta类中的fields属性指定需要创建表单元素的字段,或者使用exclude属性指定需要排除的字段。对于我们的CommentForm类,我们指定了表单只需要包含nameemailbody字段即可。

在视图中处理表单

由于提交评论的动作在文章详情页发生,所以把处理表单的功能整合进文章详情视图中会让代码更简洁。编辑views.py文件,导入Comment模型然后修改post_detail视图:

from .models import Post, Comment
from .forms import EmailPostForm, CommentForm

def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, slug=post, status="published", publish__year=year, publish__month=month, publish__day=day)
    # 列出文章对应的所有活动的评论
    comments = post.comments.filter(active=True)

    new_comment = None

    if request.method == "POST":
        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            # 通过表单直接创建新数据对象,但是不要保存到数据库中
            new_comment = comment_form.save(commit=False)
            # 设置外键为当前文章
            new_comment.post = post
            # 将评论数据对象写入数据库
            new_comment.save()
    else:
        comment_form = CommentForm()
    return render(request, 'blog/post/detail.html',
                  {'post': post, 'comments': comments, 'new_comment': new_comment, 'comment_form': comment_form})

现在post_detail视图可以显示文章及其评论,在视图中增加了一个获得当前文章对应的全部评论的QuerySet,如下:

comments = post.comments.filter(active=True)

Comments类中定义的外键的related_name属性的名称作为管理器,对post对象执行查询从而得到了所需的QuerySet。

同时还为这个视图增加了新增评论的功能。初始化了一个new_comment变量为None,用于标记一个新评论是否被创建。如果是GET请求,使用comment_form = CommentForm()创建空白表单;如果是POST请求,使用提交的数据生成表单对象并调用is_valid()方法进行验证。如果表单未通过验证,使用当前表单渲染页面以提供错误信息。如果表单通过验证,则进行如下工作:

  1. 调用当前表单的save()方法生成一个Comment实例并且赋给new_comment变量,就是下边这一行:
new_comment = comment_form.save(commit=False)
表单对象的`save()`方法会返回一个由当前数据构成的,表单关联的数据类的对象,并且会将这个对象写入数据库。如果指定`commit=False`,则数据对象会被创建但不会被写入数据库,便于在保存到数据库之前对对象进行一些操作。
  1. comment对象的外键关联指定为当前文章:new_comment.post = post,这样就明确了当前的评论是属于这篇文章的。
  2. 最后,调用save()方法将新的评论对象写入数据库:new_comment.save()

save()方法仅对ModelForm生效,因为Form类没有关联到任何数据模型。

为文章详情页面添加评论

已经创建了用于管理一个文章的评论的视图,现在需要修改post/detail.html来做以下的事情:

  • 展示当前文章的评论总数
  • 列出所有评论
  • 展示表单供用户添加新评论

首先要增加评论总数,编辑post/detail.html,将下列内容追加在content块内的底部:

{% with comments.count as total_comments %}
    <h2>
        {{ total_comments }} comment{{ total_comments|pluralize }}
    </h2>
{% endwith %}

我们在模板里使用了Django ORM,执行了comments.count()。在模板中执行一个对象的方法时,不需要加括号;也正因为如此,不能够执行必须带有参数的方法。{% with %}标签表示在{% endwith %}结束之前,都可以使用一个变量来代替另外一个变量或者值。

{% with %}标签经常用于避免反复对数据库进行查询和向模板传入过多变量。

这里使用了pluralize模板过滤器,用于根据total_comments的值显示复数词尾。将在下一章详细讨论模板过滤器。

如果值大于1,pluralize过滤器会返回一个带复数词尾"s"的字符串,实际渲染出的字符串会是0 comments1 comment2 comments或者N comments

然后来增加评论列表的部分,在post/detail.html中上述代码之后继续追加:

{% for comment in comments %}
    <div class="comment">
        <p class="info">
            Comment {{ forloop.counter }} by {{ comment.name }}
            {{ comment.created }}
        </p>
        {{ comment.body|linebreaks }}
    </div>
{% empty %}
    <p>There are no comments yet.</p>
{% endfor %}

这里使用了{% for %}标签,用于循环所有的评论数据对象。如果comments对象为空,则显示一条信息提示用户没有评论。使用{{ forloop.counter }}可以在循环中计数。然后,显示该条评论的发布者,发布时间和评论内容。

最后是显示表单或者一条成功信息的部分,在上述代码后继续追加:

{% if new_comment %}
    <h2>Your comment has been added.</h2>
{% else %}
    <h2>Add a new comment</h2>
    <form action="." method="post">
    {{ comment_form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Add comment"></p>
    </form>
{% endif %}

这段代码的逻辑很直白:如果new_comment对象存在,显示一条成功信息,其他情况下则用as_p方法渲染整个表单以及CSRF token。在浏览器中打开http://127.0.0.1:8000/blog/,可以看到如下页面:

Django实战-博客项目_Django博客_16

使用表单添加一些评论,然后刷新页面,应该可以看到评论以发布的时间排序:

Django实战-博客项目_Django实战_17

在浏览器中打开http://127.0.0.1:8000/admin/blog/comment/,可以在管理后台中看到所有评论,点击其中的一个进行编辑,取消掉Active字段的勾,然后点击SAVE按钮。然后会跳回评论列表,Acitve栏会显示一个红色叉号表示该评论未被激活,如下图所示:

Django实战-博客项目_Django实战_18

此时返回文章详情页,可以看到被设置为未激活的评论不会显示出来,也不会被统计到评论总数中。由于有了这个active字段,可以非常方便的控制评论显示与否而不需要实际删除。

添加标签功能

在完成了评论系统之后,我们将来给文章加上标签系统。标签系统通过集成django-taggit第三方应用模块到我们的Django项目来实现,django-taggit提供一个Tag数据模型和一个管理器,可以方便的给任何模型加上标签。django-taggit的源代码位于:https://github.com/alex/django-taggit

通过pip安装django-taggit

pip install django_taggit==0.22.2

译者注:如果安装了django 2.1或更新版本,请下载最新版 django-taggit。原书的0.22.2版只能和Django 2.0.5版搭配使用。新版使用方法与0.22.2版没有任何区别。

之后在setting.py里的INSTALLED_APPS设置中增加taggit以激活该应用:

INSTALLED_APPS = [
    # ...
    'blog.apps.BlogConfig',
    'taggit',
]

打开blog应用下的models.py文件,将django-taggit提供的TaggableMananger模型管理器加入到Post模型中:

from taggit.managers import TaggableManager

class Post(models.Model):
    # ......
    tags=TaggableManager()

这个管理器可以对Post对象的标签进行增删改查。然后执行数据迁移。

现在数据库也已经同步完成了,先学习一下如何使用django-taggit模块和其tags管理器。使用python manage.py shell进入Python命令行然后输入下列命令:

之后来看如何使用,先到命令行里:

>>> from blog.models import Post
>>> post = Post.objects.get(id=1)

然后给这个文章增加一些标签,然后再获取这些标签看一下是否添加成功:

>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>, <Tag: django>]>

删除一个标签再检查标签列表:

>>> post.tags.remove('django')
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>]>

操作很简单。启动站点然后到http://127.0.0.1:8000/admin/taggit/tag/,可以看到列出taggit应用中Tag对象的管理页面:

Django实战-博客项目_Django博客_19

http://127.0.0.1:8000/admin/blog/post/点击一篇文章进行修改,可以看到文章现在包含了一个标签字段,如下所示:

Django实战-博客项目_Django博客_20

现在还需要在页面上展示标签,编辑blog/post/list.html,在显示文章的标题下边添加:

<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>

join过滤器的功能和Python字符串的join()方法很类似,打开http://127.0.0.1:8000/blog/,就可以看到在每个文章的标题下方列出了标签:

Django实战-博客项目_Django实战_21

现在来编辑post_list视图,让用户可以根据一个标签列出具备该标签的所有文章,打开blog应用的views.py文件,从django-taggit中导入Tag模型,然后修改post_list视图,让其可以额外的通过标签来过滤文章:

from taggit.models import Tag

def post_list(request, tag_slug=None):
    tag = None
    if tag_slug:
        tag = get_object_or_404(Tag, slug=tag_slug)
        object_list = object_list.filter(tags__in=[tag])
    paginator = Paginator(object_list, 3) # 3 posts in each page
    # ......

post_list视图现在工作如下:

  1. 多接收一个tag_slug参数,默认值为None。这个参数将通过URL传入
  2. 在视图中,创建了初始的QuerySet用于获取所有的已发布的文章,然后判断如果传入了tag_slug,就通过get_object_or_404()方法获取对应的Tag对象
  3. 然后过滤初始的QuerySet,条件为文章的标签中包含选出的Tag对象,由于这是一个多对多关系,所以将Tag对象放入一个列表内选择。

QuerySet是惰性的,直到模板渲染过程中迭代posts对象列表的时候,QuerySet才被求值。

最后修改,视图底部的render()方法,把tag变量也传入模板。完整的视图如下:

def post_list(request, tag_slug=None):
    object_list = Post.published.all()
    tag = None

    if tag_slug:
        tag = get_object_or_404(Tag, slug=tag_slug)
        object_list = object_list.filter(tags__in=[tag])

    paginator = Paginator(object_list, 3) # 3 posts in each page
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        posts = paginator.page(1)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)

    return render(request, 'blog/post/list.html', {'page': page, 'posts': posts, 'tag': tag})

打开blog应用的urls.py文件,注释掉PostListView那一行,取消post_list视图的注释,像下边这样:

path('', views.post_list, name='post_list'),
# path('', views.PostListView.as_view(), name='post_list'),

再增加一行通过标签显示文章的URL:

path('tag/<slug:tag_slug>/', views.post_list, name='post_list_by_tag'),

可以看到,两个URL指向了同一个视图,但命名不同。第一个URL不带任何参数去调用post_list视图,第二个URL则会带上tag_slug参数调用post_list视图。使用了一个<slug:tag_slug>获取参数。

由于我们将CBV改回为FBV,所以在blog/post/list.html里将include语句的变量改回FBV的posts

{% include "pagination.html" with page=posts %}

再增加显示文章标签的{% for %}循环的代码:

{% if tag %}
    <h2>Posts tagged with "{{ tag.name }}"</h2>
{% endif %}

如果用户访问博客,可以看到全部的文章列表;如果用户点击某个具体标签,就可以看到具备该标签的文章。现在还需改变一下标签的显示方式:

<p class="tag">
    Tags:
    {% for tag in post.tags.all %}
        <a href="{% url "blog:post_list_by_tag" tag.slug %}">{{ tag.name }}</a>
    {% if not forloop.last %}, {% endif %}
    {% endfor %}
</p>

我们通过迭代所有标签,将标签设置为一个链接,指向通过该标签对应的所有文章。通过{% url "blog:post_list_by_tag" tag.slug %}反向解析出了链接。

现在到http://127.0.0.1:8000/blog/,然后点击任何标签,就可以看到该标签对应的文章列表:

Django实战-博客项目_Django博客_22

通过相似性获取文章

在为博客添加了标签功能之后,可以使用标签来做一些有趣的事情。一些相同主题的文章会具有相同的标签,可以创建一个功能给用户按照共同标签数量的多少推荐文章。

为了实现该功能,需要如下几步:

  1. 获得当前文章的所有标签
  2. 拿到所有具备这些标签的文章
  3. 把当前文章从这个文章列表里去掉以避免重复显示
  4. 按照具有相同标签的多少来排列
  5. 如果文章具有相同数量的标签,按照时间来排列
  6. 限制总推荐文章数目

这几个步骤会使用到更复杂的QuerySet,打开blog应用的views.py文件,在最上边增加一行:

from django.db.models import Count

这是从Django ORM中导入的Count聚合函数,这个函数可以按照分组统计某个字段的数量,django.db.models还包含下列聚合函数:

  • Avg:计算平均值
  • Max:取最大值
  • Min:取最小值
  • Count:计数
  • Sum:求和

译者注:作者在此处有所保留,没有写Sum函数,此外还有Q查询

聚合查询的官方文档在https://docs.djangoproject.com/en/2.0/topics/db/aggregation/

修改post_detail视图,在render()上边加上这一段内容,缩进与render()行同级:

def post_detail(request, year, month, day, post):
    # ......
    # 显示相近Tag的文章列表
    post_tags_ids = post.tags.values_list('id',flat=True)
    similar_tags = Post.published.filter(tags__in=post_tags_ids).exclude(id=post.id)
    similar_posts = similar_tags.annotate(same_tags=Count('tags')).order_by('-same_tags','-publish')[:4]
    return render(......)

以上代码解释如下:

  1. values_list方法返回指定的字段的值构成的元组,通过指定flat=True,让其结果变成一个列表比如[1, 2, 3, ...]
  2. 选出所有包含上述标签的文章并且排除当前文章
  3. 使用Count对每个文章按照标签计数,并生成一个新字段same_tags用于存放计数的结果
  4. 按照相同标签的数量,降序排列结果,然后截取前四个结果作为最终传入模板的数据对象。

最后修改render()函数将新生成的similar_posts传给模板:

    return render(request,
        'blog/post/detail.html',
        {'post': post,
        'comments': comments,
        'new_comment': new_comment,
        'comment_form': comment_form,
        'similar_posts': similar_posts})

然后编辑blog/post/detail.html,将以下代码添加到评论列表之前:

<h2>Similar posts</h2>
    {% for post in similar_posts %}
    <p>
        <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
    </p>
{% empty %}
    There are no similar posts yet.
{% endfor %}

现在的文章详情页面示例如下:

Django实战-博客项目_Django博客_23

现在已经实现了该功能。django-taggit模块包含一个similar_objects()模型管理器也可以实现这个功能,可以在https://django-taggit.readthedocs.io/en/latest/api.html查看django-taggit的所有模型管理器使用方法。

还可以用同样的方法在文章详情页为文章添加标签显示。

总结

在这一章里了解了如何使用Django的表单和模型表单,为博客添加了通过邮件分享文章的功能和评论系统。第一次使用了Django的第三方应用,通过django-taggit为博客增添了基于标签的功能。最后进行复杂的聚合查询实现了通过标签相似性推荐文章。

下一章会学习如何创建自定义的模板标签和模板过滤器,创建站点地图和RSS feed,以及对博客文章实现全文检索功能。

扩展博客功能

在上一章里,使用了基本的表单提交数据与处理表单,进行复杂的分组查询,还学习了集成第三方应用。

这一章涵盖如下的内容:

  • 自定义模板标签和模板过滤器
  • 给站点增加站点地图和订阅功能
  • 使用PostgreSQL数据库实现全文搜索

自定义模板标签和过滤器

Django提供很多内置的模板标签和过滤器,例如{% if %}{% block %},在之前已经在模板中使用过它们。关于完整的内置模板标签和过滤器可以看https://docs.djangoproject.com/en/2.0/ref/templates/builtins/

Django也允许你创建自已的模板标签,用来在页面中进行各种操作。当你想在模板中实现Django没有提供的功能时,自定义模板标签是一个好的选择。

自定义模板标签

Django提供下边的两个函数可以简单快速地创建自定义模板标签:

  • simple_tag: 处理数据并且返回字符串
  • inclusion_tag: 处理数据并返回一个渲染的模板

所有的自定义标签,只能够在模板中使用。

blog应用目录里新建一个目录templatetags,然后在其中创建一个空白的__init__.py,再创建一个文件blog_tags.py,文件结构如下:

blog/
    __init__.py
    models.py
    ...
    templatetags/
        __init__.py
        blog_tags.py

注意这里的命名很关键,一会在模板内载入自定义标签的时候就需要使用这个包的名称(templatetags)。

先创建一个简单的标签,在刚刚创建的blog_tags.py里写如下代码:

from django import template
from ..models import Post

register = template.Library()

@register.simple_tag
def total_posts():
    return Post.published.count()

我们创建了一个标签返回已经发布的文章总数。每个模板标签的模块内需要一个register变量,是template.Library的实例,用于注册自定义的标签。然后创建了一个Python函数total_posts,用@register.simple_tag装饰器将其注册为一个简单标签。Django会使用这个函数的名称作为标签名称,如果想使用其他的名称,可以通过name属性指定,例如@register.simple_tag(name='my_tag')

在添加了新的自定义模板标签或过滤器之后,必须重新启动django服务才能在模板中生效。

在模板内使用自定义标签之前,需要使用{% load %}在模板中引入自定义的标签,像之前提到的那样,使用创建的包的名字作为load的参数。

打开blog/templates/base.html模板,在最上边添加{% load blog_tags %},然后使用自定义标签{% total_posts %},这个模板最后看起来像这样:

{% load blog_tags %}
{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
        <link href="{% static "css/blog.css" %}" rel="stylesheet">
    </head>
    <body>
        <div id="content">
            {% block content %}
            {% endblock %}
        </div>
        <div id="sidebar">
            <h2>My blog</h2>
            <p>This is my blog. I've written {% total_posts %} posts so far.</p>
        </div>
    </body>
</html>

启动站点然后到http://127.0.0.1:8000/blog/,应该可以看到总文章数被显示在了侧边栏:

Django实战-博客项目_Django博客_24

自定义标签威力强大之处在于不通过视图就可以处理数据和添加到模板中。

现在再来创建一个用于在侧边栏显示最新发布的文章的自定义标签。这次通过inclusion_tag渲染一段HTML代码。编辑blog_tags.py文件,添加如下内容:

@register.inclusion_tag('blog/post/latest_posts.html')
def show_latest_posts(count=5):
    latest_posts = Post.published.order_by('-publish')[:count]
    return {'latest_posts': latest_posts}

在上边的代码里,使用@register.inclusion_tag装饰器装饰了自定义函数show_latest_posts,同时指定了要渲染的模板为blog/post/latest_posts.html。我们的模板标签还接受一个参数count,通过Post.published.order_by('-publish')[:count]切片得到指定数量的最新发布的文章。注意这个自定义函数返回的是一个字典对象而不是一个具体的值。inclusion_tag必须返回一个类似给模板传入变量的字典,用于在blog/post/latest_posts.html中取得数据并渲染模板。刚刚创建的这一切在模板中以类似{% show_latest_posts 3 %}的形式来使用。

那么在模板里如何使用呢,这是一个带参数的tag,就像之前使用内置的那样,在标签后边加参数: {% show_latest_posts 3 %}

blog/post/目录下创建latest_posts.html文件,添加下列代码:

<ul>
    {% for post in latest_posts %}
    <li>
        <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
    </li>
    {% endfor %}
</ul>

在上边的代码里,使用lastest_posts显示出了一个未排序的最新文章列表。然后编辑blog/base.html,加入新标签以显示最新的三篇文章:

<div id="sidebar">
    <h2>My blog</h2>
    <p>This is my blog. I've written {% total_posts %} posts so far.</p>
    <h3>Latest posts</h3>
    {% show_latest_posts 3 %}
</div>

模板中调用了自定义标签,然后传入了一个参数3,之后这个标签的位置会被替换成被渲染的模板。

现在返回浏览器,刷新页面,可以看到侧边栏显示如下:

Django实战-博客项目_Django博客_25

最后再来创建一个simple_tag,将数据存放在这个标签内,而不是像我们创建的第一个标签一样直接展示出来。我们使用这种方法来显示评论最多的文章。编辑blog_tags.py,添加下列代码:

from django.db.models import Count

@register.simple_tag
def get_most_commented_posts(count=5):
    return Post.published.annotate(total_comments=Count('comments')).order_by('-total_comments')[:count]

在上边的代码里,使用annotate,对每篇文章的评论进行计数然后按照total_comments字段降序排列,之后使用[:count]切片得到评论数量最多的特定篇文章,

除了Count之外,Django提供了其他聚合函数AvgMaxMinSum,聚合函数的详情可以查看https://docs.djangoproject.com/en/2.0/topics/db/aggregation/

编辑blog/base.html把以下代码追加到侧边栏<div>元素内部:

<h3>Most commented posts</h3>
{% get_most_commented_posts as most_commented_posts %}
<ul>
    {% for post in most_commented_posts %}
        <li>
            <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
        </li>
    {% endfor %}
</ul>

在这里使用了as将我们的模板标签保存在一个叫做most_commented_posts变量中,然后展示其中的内容。

现在打开浏览器刷新页面,可以看到新的页面如下:

Django实战-博客项目_Django实战_26

关于自定义模板标签的详情可以查看https://docs.djangoproject.com/en/2.0/howto/custom-template-tags/

自定义模板过滤器

Djangon内置很多模板过滤器用于在模板内修改变量。模板过滤器实际上是Ptyhon函数,接受1或2个参数-其中第一个参数是变量,第二个参数是一个可选的变量,然后返回一个可供其他模板过滤器操作的值。一个模板过滤器类似这样:{{ variable|my_filter }},带参数的模板过滤器类似:{{ variable|my_filter:"foo" }},可以连用过滤器,例如:{{ variable|filter1|filter2 }}

我们来通过自定义过滤器使我们的博客文章可以支持Markdown语法,然后将其转换成对应的HTML格式。Markdown是一种易于使用的轻型标记语言而且可以方便的转为HTML。可以在这里查看Markdown语法的详情:https://daringfireball.net/projects/markdown/basics

先通过pip安装Python的Markdown模块:

pip install Markdown==2.6.11

然后编辑blog_tags.py,添加如下内容:

from django.utils.safestring import mark_safe
import markdown

@register.filter(name='markdown')
def markdown_format(text):
    return mark_safe(markdown.markdown(text))

我们使用和模板标签类似的方式注册了模板过滤器,为了不使我们的函数和markdown模块重名,将我们的函数命名为markdown_format,但是指定了模板中的标签名称为markdown,这样就可以通过{{ variable|markdown }}来使用标签了。mark_safe用来告诉Django该段HTML代码是安全的,可以将其渲染到最终页面中。默认情况下,Django对于生成的HTML代码都会进行转义而不会当成HTML代码解析,只有对mark_safe标记的内容才会正常解析,这是为了避免在页面中出现危险代码(如添加外部JavaScript文件的代码)。

然后在blog/post/list.htmlblog/post/detail.html中的{% extends %}之后引入自定义模板的模块:

{% load blog_tags %}

post/detail.html中,找到下边这行:

{{ post.body|linebreaks }}

将其替换成:

{{ post.body|markdown }}

然后在post/list.html中,找到下边这行:

{{ post.body|truncatewords:30|linebreaks }}

将其替换成:

{{ post.body|markdown|truncatewords_html:30 }}

truncatewords_html过滤器不会截断未闭合的HTML标签。

浏览器中打开http://127.0.0.1:8000/admin/blog/post/add/然后写一段使用Markdown语法的正文:

This is a post formatted with markdown
--------------------------------------
*This is emphasized* and this is more emphasized.
Here is a list:

* One
* Two
* Three

And a [link to the Django website](https://www.djangoproject.com/)

然后在浏览器中查看刚添加的文章,可以看到如下的结果:

Django实战-博客项目_Django博客_27

可以看到,自定义模板过滤器在需要自定义格式的时候非常好用。可在https://docs.djangoproject.com/en/2.0/howto/custom-template-tags/#writing-custom-template-filters找到更多关于自定义过滤器的信息。

创建站点地图

Django带有站点地图功能框架,可以根据网站内容动态的生成站点地图。站点地图是一个XML文件,用于给搜索引擎提供信息,可以帮助搜索引擎爬虫索引站点的内容。

Django的站点地图框架是django.contrib.sites。如果使用同一个Django项目运行多个站点,站点地图功能允许为每个站点创建单独的站点地图。为了使用站点地图功能,需要启用django.contrib.sitesjango.contrib.sitemaps,将这两个应用添加到settings.pyINSTALLED_APPS设置中:

SITE_ID = 1
# Application definition
INSTALLED_APPS = [
    # ...
    'django.contrib.sites',
    'django.contrib.sitemaps',
]

由于添加了新应用,需要执行数据迁移。迁移完成之后,在blog应用目录内创建sitemaps.py文件并添加如下代码:

from django.contrib.sitemaps import Sitemap
from .models import Post

class PostSitemap(Sitemap):
    changefreq = 'weekly'
    priority = 0.9

    def items(self):
        return Post.published.all()

    def lastmod(self, obj):
        return obj.updated

我们通过继承django.contrib.sitemapsSitemap类创建了一个站点地图对象。changefreqpriority属性表示文章页面更新的频率和这些文章与站点的相关性(最大相关性为1)。使用items()方法返回这个站点地图所需的QuerySet,Django默认会调用数据对象的get_absolute_url()获取对应的URL,如果想手工指定具体的URL,可以为PostSitemap添加一个location方法。lastmod方法接收items()返回的每一个数据对象然后返回其更新时间。changefreqpriority可以通过定义方法也可以作为属性名进行设置。站点地图的详细使用可以看官方文档:https://docs.djangoproject.com/en/2.0/ref/contrib/sitemaps/

最后就是配置站点地图对应的URL,打开项目的根urls.py,添加如下代码:

from django.urls import path, include
from django.contrib import admin
from django.contrib.sitemaps.views import sitemap
from blog.sitemaps import PostSitemap

sitemaps = {'posts': PostSitemap,}

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace='blog')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap')
]

在上述代码中,导入了需要的库并且定义了一个站点地图的字典。将sitemap.xml的路径匹配到sitemap视图。sitemaps字典会被传递给sitemap视图。现在启动站点然后在浏览器中打开http://127.0.0.1:8000/sitemap.xml,可以看到如下的输出:

<?xml version="1.0" encoding="utf-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>http://example.com/blog/2017/12/15/markdown-post/</loc>
        <lastmod>2017-12-15</lastmod>
        <changefreq>weekly</changefreq>
        <priority>0.9</priority>
    </url>
    <url>
        <loc>
            http://example.com/blog/2017/12/14/who-was-django-reinhardt/
        </loc>
        <lastmod>2017-12-14</lastmod>
        <changefreq>weekly</changefreq>
        <priority>0.9</priority>
    </url>
</urlset>

其中的每个文章的URL都是由get_absolute_url()方法生成的。lastmod标签的内容是最后更新的时间,和在类中定义的一样。changefreqpriority标签也包含对应的值。可以看到站点名为example.com,这个名称来自于数据库存储的Site对象,这是我们在为站点地图应用进行数据迁移的时候默认生成的一个对象。打开http://127.0.0.1:8000/admin/sites/site/,可以看到类似下边的界面:

Django实战-博客项目_Django博客_28

上边的截图里包含刚才使用的的主机名,可以修改成自己想要的主机名。可以将其修改成localhost:8000以使用本地地址生成URL。如下图所示:

Django实战-博客项目_Django实战_29

设置之后,URL就会使用本地地址。在生产环境中,需要在此处设置正常的主机和站点名。

创建订阅功能

Django内置一些功能,采用和创建站点地图类似的方法为站点增加RSS或者Atom订阅信息。订阅信息是一个特定的数据格式,通常是XML文件,用于向用户提供这个网站的更新数据,用户通过一个订阅代理程序,订阅这个网站的feed,就可以接收到新的内容通知。

blog应用目录下新建feeds.py文件并添加如下代码:

from django.contrib.syndication.views import Feed
from django.template.defaultfilters import truncatewords
from .models import Post

class LastestPostFeed(Feed):
    title = 'My blog'
    link = '/blog/'
    description = 'New posts of my blog.'

    def items(self):
        return Post.published.all()[:5]

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        return truncatewords(item.body, 30)

译者注:item_description(self, item)这个函数并没有对post.body进行处理,所以会返回未经处理的markdown代码,在不支持markdown的Feed阅读器里会出现问题,读者可以修改该函数,调用markdown库输出转换后的字符串。

这段代码首先继承了内置的Feed类,titlelinkdescription属性分别对应XML文件的<title><link><description>标签。

items()方法用于获得订阅信息要使用的数据对象,这里只取了最新发布的5篇文章。item_title()item_description()方法接收每一个数据对象并且分别返回标题和正文前30个字符。

现在为配置订阅路径,编辑blog/urls.py,导入LatestPostsFeed然后配置一条新路由:

from .feeds import LatestPostsFeed

urlpatterns = [
    # ...
    path('feed/', LatestPostsFeed(), name='post_feed'),
]

打开地址http://127.0.0.1:8000/blog/feed/即可看到feed内容,包含最新的5篇文章:

<?xml version="1.0" encoding="utf-8"?>
    <rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title>My blog</title>
        <link>http://localhost:8000/blog/</link>
        <description>New posts of my blog.</description>
        <atom:link href="http://localhost:8000/blog/feed/" rel="self"/>
        <language>en-us</language>
        <lastBuildDate>Fri, 15 Dec 2017 09:56:40 +0000</lastBuildDate>
        <item>
            <title>Who was Django Reinhardt?</title>
            <link>http://localhost:8000/blog/2017/12/14/who-was-djangoreinhardt/</
        link>
        <description>Who was Django Reinhardt.</description>
        <guid>http://localhost:8000/blog/2017/12/14/who-was-djangoreinhardt/</
        guid>
        </item>
        ...
    </channel>
</rss>

如果用一个RSS阅读器打开这个链接,就可以在其界面里看到对应信息。

最后一步是在侧边栏添加订阅本博客的链接,在blog/base.html里的侧边栏

里追加:

 


之后就可以在http://127.0.0.1:8000/blog/看到订阅链接,类似下图:

Django实战-博客项目_Django博客_30

增加全文搜索功能

现在可以为博客添加搜索功能。Django ORM可以使用contains或类似的icontains过滤器执行简单的匹配任务。比如:

from blog.models import Post
Post.objects.filter(body__contains='framework')

然而,如果要执行更加复杂的搜索,比如通过权重或者相似性,就必须使用一个全文搜索引擎(full-text search engine)

Django的全文检索功能基于PostgreSQL数据库的全文搜索特性,所以这个全文检索功能不能用于Django ORM支持的其他种类的数据库。PostgreSQL的全文搜索介绍在https://www.postgresql.org/docs/10/static/textsearch.html

虽然Django ORM通过面向对象抽象,可以不依赖于具体的数据库,但是用于PostgreSQL的一部分功能无法用于其他数据库。

自定义模板过滤器

现在blog项目使用的是Python自带的SQLlite数据库,对于开发而言已经足够。在生产环境中,需要使用诸如MySQL,PostgreSQL和Oracle等更强力的数据库。为了实现全文搜索功能,我们将转而使用PostgreSQL。

在Linux环境下,需要先安装PostgreSQL和Python的相关依赖:

sudo apt-get install libpq-dev python-dev

之后使用下列命令安装PostgreSQL:

sudo apt-get install postgresql postgresql-contrib

如果使用MacOS X或者Windows,到https://www.postgresql.org/download/查看安装说明。

在安装完之后,还需要为python安装psycopg2模块:

pip install psycopg2==2.7.4

译者注:使用 pip install psycopg2-binary 命令安装 psycopg2 最新版模块。

在PostgreSQL中创建一个名叫blog的用户,供项目使用。在系统命令行中输入下列命令:

su postgres
createuser -dP blog

会被提示输入密码。创建用户成功之后,创建一个名叫blog的数据库并将所有权设置给blog用户:

createdb -E utf8 -U blog blog

译者注:PostgreSQL在Linux安装后会创建一个postgres用户,使用该用户身份可以登陆PostgreSQL数据库进行操作。

之后编辑settings.py文件中的DATABASES设置:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'blog',
        'USER': 'blog',
        'PASSWORD': '*',
    }
}

这里我们将默认的数据库修改成了PostgreSQL,之后执行数据迁移和创建超级用户。然后可以在http://127.0.0.1:8000/admin/登录管理后台。由于更换了数据库,博客应用内没有任何文章数据,录入一些数据,为之后使用全文搜索做准备。

执行简单搜索

编辑settings.py文件,将django.contrib.postgres加入到INSTALLED_APPS中:

INSTALLED_APPS = [
    # ...
    'django.contrib.postgres',
]

激活还应用后,现在可以通过search参数进行搜索:

from blog.models import Post
Post.objects.filter(body__search='django')

这个QuerySet会使用PostgreSQL为body字段创建内容是'django'字符串的搜索向量和一个查询,通过匹配查询和结果向量,返回最后的结果。

执行简单搜索

可能想在多个字段中进行检索。在这种情况下,需要定义一个SearchVector搜索向量对象,来创建一个针对Post模型的titlebody进行搜索的向量:

from django.contrib.postgres.search import SearchVector
from blog.models import Post

Post.objects.annotate(search=SearchVector('title','body'),).filter(search='poem')

使用分组函数然后定义了两个字段的向量,之后使用查询,就可以得到最终的结果。

全文搜索是一个密集计算过程,如果要检索的数据多于几百行,最好创建一个匹配搜索向量的索引,Django提供了一个SearchVectorField字段在模型中定义搜索向量。具体可以参考https://docs.djangoproject.com/en/2.0/ref/contrib/postgres/search/#performance

创建搜索视图

现在我们可以创建一个视图用于让用户执行搜索。首先需要一个表单让用户输入要查询的数据,编辑blog应用的forms.py增加下面的表单:

class SearchForm(forms.Form):
    query = forms.CharField()

query字段用于输入查询内容,然后编辑blog应用的views.py文件,然后添加如下代码:

from django.contrib.postgres.search import SearchVector
from .forms import EmailPostForm, CommentForm, SearchForm

def post_search(request):
    form = SearchForm()
    query = None
    results = []
    if 'query' in request.GET:
        form = SearchForm(request.GET)
        if form.is_valid():
            query = form.cleaned_data['query']
            results = Post.objects.annotate(search=SearchVector('title', 'slug', 'body'), ).filter(search=query)
    return render(request, 'blog/post/search.html', {'query': query, "form": form, 'results': results})

这个视图先初始化空白SearchForm表单,通过GET请求附加URL参数的方式提交表单。如果request.GET字典中存在query参数且通过了表单验证,就执行搜索并返回结果。

视图编写完毕,需要编写对应的模板,在/blog/post/目录下创建search.html文件,添加如下代码:

{% extends 'blog/base.html' %}

{% block title %}
Search
{% endblock %}

{% block content %}
{% if query %}
    <h1>Post containing {{ query }}</h1>
    <h3>
    {% with results.count as total_results %}
        Found {{ total_results }} result{{ total_results|pluralize }}
    {% endwith %}
    </h3>
    {% for post in results %}
        <h4>
            <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
        </h4>
        {{ post.body|truncatewords:5 }}
    {% empty %}
        <p>There are no results for your query.</p>
    {% endfor %}
    <p><a href="{% url 'blog:post_search' %}">Search again</a></p>
{% else %}
    <h1>Search for posts</h1>
    <form action="." method="get">
    {{ form.as_p }}
        <input type="submit" value="Search">
    </form>
{% endif %}
{% endblock %}

就像在视图中的逻辑一样,我们通过query参数存在与否判断表单是否提交。默认不提交表单的页面,显示表单和一个搜索按钮,进行搜索后则显示结果总数和搜索到的文章列表。

由于表单里配置了反向解析,所以编辑blog应用的urls.py

path('search/', views.post_search, name='post_search'),

现在启动站点,到http://127.0.0.1:8000/blog/search/可以看到表单页面如下:

Django实战-博客项目_Django实战_31

输入查询内容然后点击搜索按钮,可以看到查询结果,如下所示:

Django实战-博客项目_Django博客_32

现在我们就创建了全文搜索功能了。

词干提取与搜索排名

Django提供了一个SearchQuery类将一个查询词语转换成一个查询对象,默认会通过词干提取算法(stemming algorithms)转换成查询对象,用于更好的进行匹配。在查询时候还可能会依据相关性进行排名。PostgreSQL提供了一个排名功能,按照被搜索内容在一条数据里出现的次数和频率进行排名。

编辑blog应用的views.py文件:

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

然后找到下边这行:

results = Post.objects.annotate(search=SearchVector('title', 'body'),).filter(search=query)

替换成如下内容:

search_vector = SearchVector('title', 'body')
search_query = SearchQuery(query)
results = Post.objects.annotate(search=search_vector,
                rank=SearchRank(search_vector, search_query)
          ).filter(search=search_query).order_by('-rank')

在上边的代码中,先创建了一个SearchQuery对象,用其过滤结果。之后使用SearchRank方法将结果按照相关性排序。打开http://127.0.0.1:8000/blog/search/并且用不同的词语来测试搜索。下边是使用'django'搜索的示例:

Django实战-博客项目_Django博客_33

搜索权重

当按照相关性进行搜索时,可以给不同的向量赋予不同的权重,从而影响搜索结果。例如,可以对在标题中搜索到的结果给予比正文中搜索到的结果更大的权重。编辑blog应用的views.py文件:

search_vector = SearchVector('title', weight='A') + SearchVector('body', weight='B')
results = Post.objects.annotate(
                search=search_vector,
                rank=SearchRank(search_vector, search_query)
            ).filter(rank__gte=0.3).order_by('-rank')

在上边的代码中,给title和body字段的搜索向量赋予了不同的权重。默认的权重DCBA分别对应 0.10.20.41。我们给title字段的搜索向量赋予权重1.0,给body字段的搜索向量的权重是0.4,说明文章标题的重要性要比正文更重要,最后设置了只显示综合权重大于0.3的搜索结果。

三元相似性搜索

还有一种搜索方式是三元相似性搜索。三元指的是三个连续的字符。通过比较两个字符串里,有多少个三个连续的字符相同,可以检测这两个字符串的相似性。这种搜索方式对于不同语言中的相近单词很高效。

如果要在PostgreSQL中使用三元检索,必须安装一个pg_trgm扩展。

在系统命令行执行下列命令连接到数据库:

psql blog

然后输入下列数据库指令:

CREATE EXTENSION pg_trgm

然后编辑视图来增加三元相似搜索功能,编辑blog应用的views.py,这一次需要导入的新组件:

from django.contrib.postgres.search import TrigramSimilarity

然后将Post搜索查询对象替换成如下这样:

results = Post.objects.annotate(
              similarity=TrigramSimilarity('title',query),
              ).filter(similarity__gte=0.1).order_by('-similarity')

打开http://127.0.0.1:8000/blog/search/,然后试验不同的三元相似锁边,下边的例子显示了使用yango想搜索django的结果:

Django实战-博客项目_Django实战_34

现在就为我们的博客创建了一个强力的搜索引擎。关于在Django中使用PostgreSQL的全文搜索,可以参考https://docs.djangoproject.com/en/2.0/ref/contrib/postgres/search/

使用其他全文搜索引擎

除了常用的PostgreSQL之外,还有Solr和Elasticsearch等常用的全文搜索引擎,可以使用Haystack来将其集成到Django中。Haystack是一个Django应用,作为一个搜索引擎的抽象层工作。提供了与Django的QuerySet非常类似的API供执行搜索操作。关于Haystack的详情可以查看:http://haystacksearch.org/

总结

在这一章学习了创建自定义的模板标签和过滤器,用于提供自定义的功能。还创建了站点地图用于搜索引擎优化和RSS feed为用户提供订阅功能。之后将站点数据库改用PostgreSQL从而实现了全文搜索功能。