目录

​Django2实战示例 第一章 创建博客应用​

​Django2实战示例 第二章 增强博客功能​

​Django2实战示例 第三章 扩展博客功能​

​Django2实战示例 第四章 创建社交网站​

​Django2实战示例 第五章 内容分享功能​

​Django2实战示例 第六章 追踪用户行为​

​Django2实战示例 第七章 创建电商网站​

​Django2实战示例 第八章 管理支付与订单​

​Django2实战示例 第九章 扩展商店功能​

​Django2实战示例 第十章 创建在线教育平台​

​Django2实战示例 第十一章 渲染和缓存课程内容​

​Django2实战示例 第十二章 创建API​

​Django2实战示例 第十三章 上线​

第二章 增强博客功能

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

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

1通过邮件分享文章

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

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

1.1使用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>​​元素。

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

1.2通过视图控制表单

现在需要写一个视图,用于处理表单提交来的数据,当表单成功提交的时候发送电子邮件。编辑​​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发送邮件了。

1.3使用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 = ''
EMAIL_HOST_USER = 'your_account@'
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@', ['your_account@'], fail_silently=False)


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

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

Django2实战示例 第二章 增强博客功能_表单

现在我们把发送邮件的功能加入到视图中,编辑​​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'),
]


1.4在模板中渲染表单

在创建表单,视图和配置好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_ul​​和​​as_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/​​,点击任意文章查看详情页,在文章的正文下会出现分享链接,如下所示:

Django2实战示例 第二章 增强博客功能_数据_02

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

Django2实战示例 第二章 增强博客功能_字段_03

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

Django2实战示例 第二章 增强博客功能_表单_04

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

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

2创建评论系统

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

  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会使用模型的小写名加上​​_set​​(​​comment_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/​​查看管理站点,会看到新的模型已经被加入到管理后台中:

Django2实战示例 第二章 增强博客功能_Django2 By Example_05

2.1根据模型创建表单

在发送邮件的功能里,采用继承​​forms.Form​​类的方式,自行编写各个字段创建了一个表单。Django对于表单有两个类:​​Form​​和​​ModelForm​​。这次我们使用​​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​​类,我们指定了表单只需要包含​​name​​,​​email​​和​​body​​字段即可。

2.2在视图中处理表单

由于提交评论的动作在文章详情页发生,所以把处理表单的功能整合进文章详情视图中会让代码更简洁。编辑​​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​​类没有关联到任何数据模型。

2.3为文章详情页面添加评论

已经创建了用于管理一个文章的评论的视图,现在需要修改​​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.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/​​,可以看到如下页面:

Django2实战示例 第二章 增强博客功能_数据_06

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

Django2实战示例 第二章 增强博客功能_Django2 By Example_07

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

Django2实战示例 第二章 增强博客功能_Django2 By Example_08

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

3添加标签功能

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

Django2实战示例 第二章 增强博客功能_数据_09

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

Django2实战示例 第二章 增强博客功能_数据_10

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

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


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

Django2实战示例 第二章 增强博客功能_数据_11

现在来编辑​​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 "{{ }}"</h2>
{% endif %}


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

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


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

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

Django2实战示例 第二章 增强博客功能_数据_12

4通过相似性获取文章

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

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

  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 %}


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

Django2实战示例 第二章 增强博客功能_表单_13

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

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

总结

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

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