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

目录

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>元素。

字段验证也依赖于字段属性。例如: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})

这段代码的逻辑如下:

  • 定义了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 = '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,启用“允许不够安全的应用”,如下图所示:

image

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

image

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

image

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

image

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

image

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`,则数据对象会被创建但不会被写入数据库,便于在保存到数据库之前对对象进行一些操作。
  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.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/,可以看到如下页面:

image

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

image

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

image

此时返回文章详情页,可以看到被设置为未激活的评论不会显示出来,也不会被统计到评论总数中。由于有了这个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对象的管理页面:

image

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

image

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

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

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

image

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

image

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

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

image

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

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

总结

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

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

原文地址:https://www.cnblogs.com/superhin/p/15440077.html