第二章 增强博客功能

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

1通过邮件分享文章

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

1.1使用Django创建表单

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

Django使用两个类创建表单:

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/

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

这段代码的逻辑如下:

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

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

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

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

1.3使用Django发送邮件

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

如果无法使用任何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,启用“允许不够安全的应用”,如下图所示:

现在我们把发送邮件的功能加入到视图中,编辑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方法,将表单中的所有元素以<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/,点击任意文章查看详情页,在文章的正文下会出现分享链接,如下所示:

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

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

在某些现代浏览器上,很有可能浏览器会阻止你提交表单,提示必须完成某些字段,这是因为浏览器在提交根据表单的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会使用模型的小写名加上_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/查看管理站点,会看到新的模型已经被加入到管理后台中:

2.1根据模型创建表单

在发送邮件的功能里,采用继承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字段即可。

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,则数据对象会被创建但不会被写入数据库,便于在保存到数据库之前对对象进行一些操作。
  2. comment对象的外键关联指定为当前文章:new_comment.post = post,这样就明确了当前的评论是属于这篇文章的。
  3. 最后,调用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.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/,可以看到如下页面:

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

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

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

3添加标签功能

在完成了评论系统之后,我们将来给文章加上标签系统。标签系统通过集成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对象的管理页面:

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

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

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

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

现在来编辑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/,然后点击任何标签,就可以看到该标签对应的文章列表:

4通过相似性获取文章

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

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

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

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

from django.db.models import Count

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

译者注:作者在此处有所保留,没有写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-taggit模块包含一个similar_objects()模型管理器也可以实现这个功能,可以在https://django-taggit.readthedocs.io/en/latest/api.html查看django-taggit的所有模型管理器使用方法。

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

总结

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

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