第二章 为博客添加高级功能

2 为博客添加高级功能

上一章中,你创建了一个基础的博客应用。现在,利用一些高级特性,你要把它打造成一个功能完整的博客,比如通过邮件分享帖子,添加评论,为帖子打上标签,以及通过相似度检索帖子。在这一章中,你会学习以下主题:

  • 使用Django发送邮件
  • 在视图中创建和处理表单
  • 通过模型创建表单
  • 集成第三方应用
  • 构造复杂的QuerySet

2.1 通过邮件分享帖子

首先,我们将会允许用户通过邮件分享帖子。花一点时间想想,通过上一章学到的知识,你会如何使用视图,URL和模板来完成这个功能。现在核对一下,允许用户通过邮件发送帖子需要完成哪些操作:

  • 为用户创建一个填写名字,邮箱,收件人和评论(可选的)的表单
  • views.py中创建一个视图,用于处理post数据和发送邮件
  • blog应用的urls.py文件中,为新视图添加URL模式
  • 创建一个显示表单的模板

2.1.1 使用Django创建表单

让我们从创建分享帖子的表单开始。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)

这是你的第一个Django表单。这段代码通过继承基类Form创建了一个表单。我们使用不同的字段类型,Django可以相应的验证字段。

表单可以放在Django项目的任何地方,但惯例是放在每个应用的forms.py文件中。

name字段是一个CharField。这种字段的类型渲染为<input type="text"> HTML元素。每种字段类型都有一个默认组件,决定了该字段如何在HTML中显示。可以使用widget属性覆盖默认组件。在comments字段中,我们使用Textarea组件显示为<textarea> HTML元素,而不是默认的<input>元素。

字段的验证也依赖于字段类型。例如,emailto字段是EmailField。这两个字段都要求一个有效的邮箱地址,否则字段验证会抛出forms.ValidationError异常,导致表单无效。表单验证时,还会考虑其它参数:我们定义name字段的最大长度为25个字符,并使用required=Falsecomments字段是可选的。字段验证时,这些所有因素都会考虑进去。这个表单中使用的字段类型只是Django表单字段的一部分。在这里查看所有可用的表单字段列表。

2.1.2 在视图中处理表单

你需要创建一个新视图,用于处理表单,以及提交成功后发送一封邮件。编辑blog应用的views.py文件,添加以下代码:

from .forms import EmailPostForm

def post_share(request, post_id):
    # Retrieve post by id
    post = get_object_or_404(Post, id=post_id, status='published')
    
    if request.method == 'POST':
        # Form was submitted
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # Form fields passed validation
            cd = form.cleaned_data
            # ... send email
    else:
        form = EmailPostForm()
    return render(request, 
                    'blog/post/share.html', 
                    {'post': post, 'form': form})

该视图是这样工作的:

  • 我们定义了post_share视图,接收request对象和post_id作为参数。
  • 我们通过ID,使用get_object_or_404()快捷方法检索状态为published的帖子。
  • 我们使用同一个视图=显示初始表单和处理提交的数据。根据request.method区分表单是否提交。我们将使用POST提交表单。如果我们获得一个GET请求,需要显示一个空的表单;如果获得一个POST请求,表单会被提交,并且需要处理它。因此,我们使用request.method == 'POST'来区分这两种场景。

以下是显示和处理表单的过程:

  1. 当使用GET请求初始加载视图时,我们创建了一个新的表单实例,用于在模板中显示空表单。

form = EmailPostForm()

  1. 用户填写表单,并通过POST提交。接着,我们使用提交的数据创建一个表单实例,提交的数据包括在request.POST中:
if request.POST == 'POST':
    # Form was submitted
    form = EmailPostForm(request.POST)
  1. 接着,我们使用表单的is_valid()方法验证提交的数据。该方法会验证表单中的数据,如果所有字段都是有效数据,则返回True。如果任何字段包含无效数据,则返回False。你可以访问form.errors查看验证错误列表。
  2. 如果表单无效,我们使用提交的数据在模板中再次渲染表单。我们将会在模板中显示验证错误。
  3. 如果表单有效,我们访问form.cleaned_data获得有效的数据。该属性是表单字段和值的字典。

如果你的表单数据无效,cleaned_data只会包括有效的字段。

现在,你需要学习如何使用Django发送邮件,把所有功能串起来。

2.1.3 使用Django发送邮件

使用Django发送邮件非常简单。首先,你需要一个本地SMTP服务,或者在项目的settings.py文件中添加以下设置,定义一个外部SMTP服务的配置:

  • EMAIL_HOST:SMTP服务器地址。默认是localhost
  • EMAIL_PORT:SMTP服务器端口,默认25。
  • EMAIL_HOST_USER:SMTP服务器的用户名。
  • EMAIL_HOST_PASSWORD:SMTP服务器的密码。
  • EMAIL_USE_TLS:是否使用TLS加密连接。
  • EMAIL_USE_SSL:是否使用隐式TLS加密连接。

如果你没有本地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命令打开Python终端,如下发送邮件:

>>> 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,则表示邮件发送成功。如果你使用前面配置的Gmail发送邮件,你可能需要在这里启用低安全级别应用访问权限。

现在,我们把它添加到视图中。编辑blog应用中views.py文件的post_share视图,如下所示:

from django.core.mail import send_mail

def post_share(request, post_id):
    # Retrieve post by id
    post = get_object_or_404(Post, id=post_id, status='published')
    sent = False
    
    if request.method == 'POST':
        # Form was submitted
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # Form fields passed validation
            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, 'admin@blog.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()方法检索帖子的绝对路径。我们把这个路径作为request.build_absolute_uri()的输入,构造一个包括HTTP模式(schema)和主机名的完整URL。我们使用验证后的表单数据构造邮件的主题和内容,最后发送邮件到表单to字段中的邮件地址。

现在,视图的开发工作已经完成,记得为它添加新的URL模式。打开blog应用的urls.py文件,添加post_share的URL模式:

urlpatterns = [
    # ...
    url(r'^(?P<post_id>\d+)/share/$', views.post_share, name='post_share'),
]

2.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 {{ cd.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">

然后,我们包括了实际的表单实例。我们告诉Django使用as_p方法,在HTML的<p>元素中渲染表单的字段。我们也可以使用as_ul把表单渲染为一个无序列表,或者使用as_table渲染为HTML表格。如果你想渲染每一个字段,我们可以这样迭代字段:

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

模板标签{% csrf_token %}使用自动生成的令牌引入一个隐藏字段,以避免跨站点请求伪造(CSRF)的攻击。这些攻击包含恶意网站或程序,对你网站上的用户执行恶意操作。你可以在这里找到更多相关的信息。

上述标签生成一个类似这样的隐藏字段:

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

默认情况下,Django会检查所有POST请求中的CSRF令牌。记得在所有通过POST提交的表单中包括csrf_token标签。

编辑blog/post/detail.html模板,在{{ post.body|linebreaks }}变量之后添加链接,用于分享帖子的URL:

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

记住,我们使用Django提供的{% url %}模板标签,动态生成URL。我们使用名为blog命名空间和名为post_share的URL,并传递帖子ID作为参数来构造绝对路径的URL。

现在,使用python manage.py runserver命令启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/blog/。点击任何一篇帖子的标题,打开详情页面。在帖子正文下面,你会看到我们刚添加的链接,如下图所示:

点击Share this post,你会看到一个包含表单的页面,该页面可以通过邮件分享帖子。如下图所示:

该表单的CSS样式在static/css/blog.css文件中。当你点击Send e-mail按钮时,该表单会被提交和验证。如果所有字段都是有效数据,你会看到一条成功消息,如下图所示:

如果你输入了无效数据,会再次渲染表单,其中包括了所有验证错误:

译者注:不知道是因为浏览器不同,还是Django的版本不同,这里显示的验证错误跟原书中不一样。我用的是Chrome浏览器。

2.2 创建评论系统

现在,我们开始为博客构建评论系统,让用户可以评论帖子。要构建评论系统,你需要完成以下工作:

  • 创建一个保存评论的模型
  • 创建一个提交表单和验证输入数据的表单
  • 添加一个视图,处理表单和保存新评论到数据库中
  • 编辑帖子详情模板,显示评论列表和添加新评论的表单

首先,我们创建一个模型存储评论。打开blog应用的models.py文件,添加以下代码:

class Comment(models.Model):
    post = models.ForeignKey(Post, 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模型。它包含一个外键,把评论与单篇帖子关联在一起。这个多对一的关系在Comment模型中定义,因为每条评论对应一篇帖子,而每篇帖子可能有多条评论。从关联对象反向到该对象的关系由related_name属性命名。定义这个属性后,我们可以使用comment.post检索评论对象的帖子,使用post.comments.all()检索帖子的所有评论。如果你没有定义related_name属性,Django会使用模型名加_set(即comment_set)命名关联对象反向到该对象的管理器。

你可以在这里学习更多关于多对一的关系。

我们使用了active布尔字段,用于手动禁用不合适的评论。我们使用created字段排序评论,默认按时间排序。

刚创建的Comment模型还没有同步到数据库。运行以下命令,生成一个新的数据库迁移,反射创建的新模型:

python manage.py makemigrations blog

你会看到以下输出:

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

Django在blog应用的migrations/目录中生成了0002_comment.py文件。现在,你需要创建一个相关的数据库架构,并把这些改变应用到数据库中。运行以下命令,让已存在的数据库迁移生效:

python manage.py migrate

你会得到一个包括下面这一行的输出:

Apply blog.0002_comment... OK

我们刚创建的数据库迁移已经生效,数据库中已经存在一张新的blog_comment表。

现在我们可以添加新的模型到管理站点,以便通过简单的界面管理评论。打开blog应用的admin.py文件,导入Comment模型,并增加CommentAdmin类:

from .models import Post, Comment

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

使用python manage.py runserver命令启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/admin/。你会在Blog中看到新的模型,如下图所示:

我们的模型已经在管理站点注册,并且可以使用简单的界面管理Comment实例。

2.2.1 通过模型创建表单

我们仍然需要创建一个表单,让用户可以评论博客的帖子。记住,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为模型中的每个字段创建一个表单字段。但是,你可以使用fields列表明确告诉框架,你想在表单中包含哪些字段,或者使用exclude列表定义你想排除哪些字段。对应CommentForm,我们只使用nameemail,和body字段,因为用户只可能填写这些字段。

2.2.2 在视图中处理ModelForm

为了简单,我们将会使用帖子详情页面实例化表单,并处理它。编辑views.py文件,导入Comment模型和CommentForm表单,并修改post_detail视图,如下所示:

译者注:原书中是编辑models.py文件,应该是作者的笔误。

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)
    # List of active comments for this post
    comments = post.comments.filter(active=True)
    new_comment = None
    
    if request.method == 'POST':
        # A comment was posted
        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            # Create Comment object but don't save to database yet
            new_comment = comment_form.save(commit=False)
            # Assign the current post to comment
            new_comment.post = post
            # Save the comment to the database
            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)

我们从post对象开始创建这个QuerySet。我们在Comment模型中使用related_name属性,定义了关联对象的管理器为comments。这里使用了这个管理器。

同时,我们使用同一个视图让用户添加新评论。因此,如果视图通过GET调用,我们使用comment_form = CommentForm()创建一个表单实例。如果是POST请求,我们使用提交的数据实例化表单,并使用is_valid()方法验证。如果表单无效,我们渲染带有验证错误的模板。如果表单有效,我们完成以下操作:

  1. 通过调用表单的save()方法,我们创建一个新的Comment对象:

new_comment = comment_form.save(commit=False)

save()方法创建了一个链接到表单模型的实例,并把它存到数据库中。如果使用commit=False调用,则只会创建模型实例,而不会存到数据库中。当你想在存储之前修改对象的时候,会非常方便,之后我们就是这么做的。save()只对ModelForm实例有效,对Form实例无效,因为它们没有链接到任何模型。

  1. 我们把当前的帖子赋值给刚创建的评论:

new_comment.post = post

通过这个步骤,我们指定新评论属于给定的帖子。

  1. 最后,使用下面的代码,把新评论存到数据库中:

new_comment.save()

现在,我们的视图已经准备好了,可以显示和处理新评论了。

2.2.3 在帖子详情模板中添加评论

我们已经为帖子创建了管理评论的功能。现在我们需要修改blog/post/detail.html模板,完成以下工作:

  • 为帖子显示评论总数
  • 显示评论列表
  • 显示一个表单,用户增加评论

首先,我们会添加总评论数。打开detail.html模板,在content块中添加以下代码:

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

我们在模板中使用Django ORM执行comments.count()这个QuerySet。注意,Django模板语言调用方法时不带括号。{% with %}标签允许我们把值赋给一个变量,我们可以在{% endwith %}标签之前一直使用它。

{% with %}模板标签非常有用,它可以避免直接操作数据库,或者多次调用昂贵的方法。

我们使用了pluralize模板过滤器,根据total_comments的值决定是否显示单词comment的复数形式。模板过滤器把它们起作用变量的值作为输入,并返回一个计算后的值。我们会在第三章讨论模板过滤器。

如果值不是1,pluralize模板过滤器会显示一个“s”。上面的文本会渲染为0 comments1 comment,或者N comments。Django包括大量的模板标签和过滤器,可以帮助你以希望的方式显示信息。

现在,让我们添加评论列表。在上面代码后面添加以下代码:

{% 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对象存在,则显示一条成功消息,因为已经创建评论成功。否则渲染表单,每个字段使用一个<p>元素,以及POST请求必需的CSRF令牌。在浏览器中打开http://127.0.0.1:8000/blog/,点击一条帖子标题,打开详情页面,如下图所示:

使用表单添加两条评论,它们会按时间顺序显示在帖子下方,如下图所示:

在浏览器中打开http://127.0.0.1:8000/admin/blog/comment/,你会看到带有刚创建的评论列表的管理页面。点击某一条编辑,不选中Active选择框,然后点击Save按钮。你会再次被重定向到评论列表,该评论的Active列会显示一个禁用图标。类似下图的第一条评论:

如果你回到帖子详情页面,会发现被删除的评论没有显示;同时也没有算在评论总数中。多亏了active字段,你可以禁用不合适的评论,避免它们在帖子中显示。

2.3 增加标签功能

实现评论系统之后,我们准备为帖子添加标签。我们通过在项目中集成一个第三方的Django标签应用,来实现这个功能。django-taggit是一个可复用的应用,主要提供了一个Tag模型和一个管理器,可以很容易的为任何模型添加标签。你可以在这里查看它的源码。

首先,你需要通过pip安装django-taggit,运行以下命令:

pip install django-taggit

然后打开mysite项目的settings.py文件,添加taggitINSTALLED_APPS设置中:

INSTALLED_APPS = (
    # ...
    'blog',
    'taggit',
)

打开blog应用的models.py文件,添加django-taggit提供的TaggableManager管理器到Post模型:

from taggit.managers import TaggableManager

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

tags管理器允许你从Post对象中添加,检索和移除标签。

运行以下命令,为模型改变创建一个数据库迁移:

python manage.py makemigrations blog

你会看下以下输出:

Migrations for 'blog'
  0003_post_tags.py:
    - Add field tags to post

现在,运行以下命令创建django-taggit模型需要的数据库表,并同步模型的变化:

python manage.py migrate

你会看到迁移数据库生效的输入,如下所示:

Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying blog.0003_post_tags... OK

你的数据库已经为使用django-taggit模型做好准备了。使用python manage.py shell打开终端,学习如何使用tags管理器。

首先,我检索其中一个帖子(ID为3的帖子):

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

接着给它添加标签,并检索它的标签,检查是否添加成功:

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

最后,移除一个标签,并再次检查标签列表:

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

这很容易,对吧?运行python manage.py runserver,再次启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/admin/taggit/tag/。你会看到taggit应用管理站点,其中包括Tag对象的列表:

导航到http://127.0.0.1:8000/admin/blog/post/,点击一条帖子编辑。你会看到,现在帖子包括一个新的Tags字段,如下图所示,你可以很方便的编辑标签:

现在,我们将会编辑博客帖子,来显示标签。打开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):
    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])
        # ...

该视图是这样工作的:

  1. 该视图接收一个默认值为None的可选参数tag_slug。该参数会在URL中。
  2. 在视图中,我们创建了初始的QuerySet,检索所有已发布的帖子,如果给定了标签别名,我们使用get_object_or_404()快捷方法获得给定别名的Tag对象。
  3. 然后,我们过滤包括给定标签的帖子列表。因为这是一个多对多的关系,所以我们需要把过滤的标签放在指定列表中,在这个例子中只包含一个元素。

记住,QeurySet是懒惰的。这个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)
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        posts = paginator.page(1)
    excpet EmptyPage:
        posts = paginator.page(paginator.num_pages)
    return render(request,
                     'blog/post/list.html',
                     {'page': page,
                      'posts': posts,
                      'tag': tag})

打开blog应用的urls.py文件,注释掉基于类PostListView的URL模式,取消post_list视图的注释:

url(r'^$', views.post_list, name='post_list'),
# url(r'^$', views.PostListView.as_view(), name='post_list'),

添加以下URL模式,通过标签列出帖子:

url(r'^tag/(?P<tag_slug>[-\w]+)/$', views.post_list,
    name='post_list_by_tag'),

正如你所看到的,两个模式指向同一个视图,但是名称不一样。第一个模式不带任何可选参数调用post_list视图,第二个模式使用tag_slug参数调用视图。

因为我们使用的是post_list视图,所以需要编辑blog/post/list.hmlt模板,修改pagination使用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 %}
    {% endfof %}
</p>

现在,我们循环一篇帖子的所有标签,显示一个自定义链接到URL,以便使用该便签过滤帖子。我们用{% url "blog:post_list_by_tag" tag.slug %}构造URL,把URL名和标签的别名作为参数。我们用逗号分隔标签。

在浏览器中打开http://127.0.0.1:8000/blog/,点击某一个标签链接。你会看到由该标签过滤的帖子列表:

2.4 通过相似度检索帖子

现在,我们已经为博客帖子添加了标签,我们还可以用标签做更多有趣的事。通过便签,我们可以很好的把帖子分类。主题类似的帖子会有几个共同的标签。我们准备增加一个功能:通过帖子共享的标签数量来显示类似的帖子。在这种情况下,当用户阅读一篇帖子的时候,我们可以建议他阅读其它相关帖子。

为某个帖子检索相似的帖子,我们需要:

  • 检索当前帖子的所有标签。
  • 获得所有带这些便签中任何一个的帖子。
  • 从列表中排除当前帖子,避免推荐同一篇帖子。
  • 通过和当前帖子共享的标签数量来排序结果。
  • 如果两篇或以上的帖子有相同的标签数量,推荐最近发布的帖子。
  • 限制我们想要推荐的帖子数量。

这些步骤转换为一个复杂的QuerySet,我们需要在post_detail视图中包含它。打开blog应用的views.py文件,在顶部添加以下导入:

from django.db.models import Count

这是Django ORM的Count汇总函数。此函数允许我们执行汇总计数。然后在post_detail视图的render()函数之前添加以下代码:

# List of similar posts
post_tags_ids = post.tags.values_list('id', flat=True)
similar_posts = Post.published.filter(tags__in=post_tags_ids)\
                                    .exclude(id=post.id)
similar_posts = similar_posts.annotate(same_tags=Count('tags'))\
                             .order_by('-same_tags', '-publish')[:4]

这段代码完成以下操作:

  1. 我们获得一个包含当前帖子所有标签的ID列表。values_list()这个QuerySet返回指定字段值的元组。我们传递flat=True给它,获得一个[1, 2, 3, ...]的列表。
  2. 我们获得包含这些标签中任何一个的所有帖子,除了当前帖子本身。
  3. 我们使用Count汇总函数生成一个计算后的字段same_tags,它包含与所有查询标签共享的标签数量。
  4. 我们通过共享的标签数量排序结果(降序),共享的标签数量相等时,用publish优先显示最近发布的帖子。我们对结果进行切片,只获取前四篇帖子。

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 post yet.
{% endfor %}

推荐你在帖子详情模板中也添加标签列表,就跟我们在帖子列表模板中所做的那样。现在,你的帖子详情页面应该看起来是这样的:

译者注:需要给其它帖子添加标签,才能看到上图所示的相似的帖子。

你已经成功的推荐了相似的帖子给用户。django-taggit也包含一个similar_objects()管理器,可以用来检索共享的标签。你可以在这里查看所有django-taggit管理器。

2.5 总结

在这一章中,你学习了如何使用Django表单和模型表单。你创建了一个可以通过邮件分享网站内容的系统,还为博客创建了评论系统。你为帖子添加了标签,集成了一个可复用的应用,并创建了一个复杂的QuerySet,通过相似度检索对象。

下一章中,你会学习如何创建自定义模板标签和过滤器。你还会构建一个自定义的站点地图和帖子的RSS源,并在应用中集成一个高级的搜索引擎。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容