新功能

    这一次要给博客添加一部分功能:

  • 使用Django发送邮件分享文章
  • 直接从模型建立表单并用视图控制
  • 集成第三方应用
  • 进行更复杂的ORM查询

功能1 通过Django发送邮件分享文章

在开始之前,先想一想这部分的逻辑:

    想给用户发送邮件分享文件,有这么几个东西需要知道

  • 用户的邮件地址
  • 要有发送邮件的功能
  • 发送邮件的内容是文章的一些数据

为了实现这个功能,需要继续学习以下要点:

使用Django建立表单

Django内置了从模型生成表单的功能,这个功能的原理是:ORM模型已经使用了Django 的标准,那么表单功能也针对使用的那些字段属性,有默认的可以生成对应HTML元素的功能,这就是表单功能。此外,还可以方便快捷的生成预先定义好的表单。

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类建立的第一个标准表单,继承了内置的forms类,然后用一些字段进行了属性标记和用于验证:

  • 和ORM类似,charfield是varchar类型,要设置长度。每个字段都有一个widget,用于控制这个字段被渲染成什么HTML标签。这个CharField默认会被渲染为input type=”text”。如果不指定widget,就是使用默认widget进行渲染。可以通过widget参数指定具体渲染
  • 电子邮件地址就用EmailField
  • to也是电子邮件地址
  • 评论内容可能比较多,也有可能为空,所以设置可以不填,然后插件(实际使用的HTML标签)为Textarea部分。针对这个CharField就改变了默认的widget(对应input标签)而用了textarea

上边字段属性还一个很重要的内容就是,指定了何种字段属性,与使用的表单验证有关系。这里指定了电子邮件EmailField,就会采取验证电子邮件的方式,如果填写的内容不是电子邮件,就会报forms.ValidationError(这是后端验证,和前端是彼此独立的)。这里还有的验证是maxlength以及默认的required = True。

关于Form Field 的官方文档在这里

通过视图控制表单

前边写好了类,现在需要写一个视图,用于两个功能,一是将表单展示到页面上,二是需要接收和处理表单提交过来的玩意。

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":
        # 提交表单是POST请求
        form = EmailPostForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
        #  如果验证通过,则发送邮件
    else:
        # 如果是GET请求,就用表单返回页面
        form = EmailPostForm()
        return render(request, 'blog/post/share.html', {'post': post, 'form': form})

这里需要解释的有:

  • get_object_or_404前边已经使用过,是从数据表里直接拿数据,这里加了一个参数是post_id,这个快捷函数实际上是在模型管理器上调用.get()方法,只能返回一个对象。
  • request.Method 表示 HTTP请求的种类,对于提交表单来说,是POST请求,普通的访问页面,则是GET请求
  • 表单类的is_valid()用于检测当前表单的每一个数据项是否都通过验证,如果全部通过验证,则数据会保存在.cleaned_data这样一个属性里,以类似字典的方式存储。如果有错误,错误会保存在forms.error里。注意即使验证失败,cleaned_data里也会有验证成功的数据,不包含验证失败的那部分数据。
  • 自定义了form类后,就可以用自定义的类实例化一个对象。如果用数据来实例化参数就是request.POST,如果是生成一个默认的,就不传任何参数。
  • 如果是普通的GET请求,就返回一个空白的表单供填写。
  • 页面初始载入的时候,默认是GET请求,所以一开始到这个页面的时候,必定是一个GET请求,返回文章和对应的空白表单供填写。
  • 注意,用内置表单渲染的页面,才能直接用对应的request.POST作为参数来实例化表单类,否则需要自定义的更加详细或者干脆自己取数据进行验证,后边会学到。

这里还没有写的逻辑是,如果表单验证失败,应该返回一个页面,将用户填的数据原样显示在表单里,然后在页面上显示对应的错误提示。

share.html也还没有建立,从现在可以看到视图即业务逻辑是核心,所有的内容都围绕视图做文章。

用Django发送邮件

在Django里发送邮件其实很简单,首先需要SMTP服务器,一般注册的邮件商都提供这个服务,剩下的就是把SMTP的东西填入到Django 的 settings.py里即可:

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

如果没有邮件服务器,则可以在settings.py里加一句话:

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

这样会把所有的邮件内容显示在控制台。

在自己的Django内配置一下邮件服务器,就用新浪的邮件服务器:

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = False
EMAIL_USE_SSL = False
EMAIL_HOST = 'smtp.vip.sina.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = 'lee0709@vip.sina.com'
EMAIL_HOST_PASSWORD = '******'
DEFAULT_FROM_EMAIL = 'lee0709@vip.sina.com'

之后可以在导入django 的python环境中试验一下发送邮件,其实是使用的python的smtp模块服务:

from django.core.mail import send_mail
send_mail('gegege','gegege2','lee0709@vip.sina.com',['lee0709@vip.sina.com'],fail_silently=False)

试验成功,这个send_mail的各项参数分别是标题,邮件内容,登录SMTP的用户名,和收件人地址列表,最后一个参数表示如果发送失败就提示错误,而不是静默。在之后的视图函数中就能够发送邮件了:

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":
        # 提交表单是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:
        # 如果是GET请求,就用表单返回页面
        form = EmailPostForm()
    return render(request, 'blog/post/share.html', {'post': post, 'form': form, 'sent': sent})

函数变得复杂了一些,但其实上没有什么难度,当表单通过验证的时候,就拿到数据以及文章的链接(用了数据类里自己写的那个方法)。然后标题和邮件正文用表单中的数据进行填充,要发送给谁是用户填写的to字段,之后调用发送邮件的功能。

多设置了一个sent变量,记录邮件发送的状态,以返回给页面。

视图写好了,现在给视图配置url,因为是blog的功能,所以在blog的urls.py配置:

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

这里实际上匹配的URL是 blog/x/share 这样的地址,可以直接跳到对应的文章的分享页。

可见文章的id很重要,视图和URL配置好了,剩下的就是编写页面了,然后需要考虑视图函数中传入的那些变量。

在模板中使用表单类

编写模板之前看一下视图函数传给模板的变量和需要从模板中拿到的变量:

  • 拿到的POST表单数据
  • 从URL拿到的文章ID
  • 向模板传入邮件是否发送成功的变量 sent
  • 向模板传入文章对象post
  • 向模板传入表单对象form

看来首先需要解决的问题就是,模板在表单内的展示,就是操作form对象,在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 %}

前边的继承模板无需解释,主要是if这里,如果发送成功,则显示一条信息,用于表示发送成果。如果没有发送成功或者是默认过来,则展示页面。

这里的核心是把{{form}}写在一个HTML的form标签里,还需要加上CSRF_token,再添加一个submit按钮,可见form对象只提供数据部分的生成,而不是整个表单。

这里的.as_p是指让Django把每个字段都显示成带有段落标签的HTML。这是为了简单处理,标准做法是应该加上label标签和其他内容,然后采用CSS样式。还有as_ul和as_table,如果都不想用,还可以用循环来迭代显示每个字段,类似:

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

CSRF_TOKEN无需再多解释,默认的Django 的CSRF_TOKEN是这样的:

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

用JS操作的时候记住 CRSF_TOKEN的属性。

然后每个分享页面,其实是通过文章详情页面链接过来的,也就是给每个文章加一个分享链接,所以还需要修改blog/post/detail.html,增加一个链接:

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

这里使用到了{% url %}标签,这是一个关键点叫做反向解析,这个标签的意思,就是把blog命名空间里的:post_share的路径解析到这个地方

然后这里又是关键点:由于blog:post_share里的是一个匹配的表达式: '<int:post_id>/share/', 没有具体对应的路径,所以{% url “blog:post_share” post.id %}后边跟着的 post.id是参数,值就会被放到 <int:post_id>的位置,这样就根据当前页面的文章,动态的生成对应的分享链接。

启动页面

启动页面,发现在每个文章的详情页面,都出现了新的链接,点进去了就是新的页面,可以填写然后发送。

新的详情页面如下:

从详情页面点击分享链接后的页面如下:

这个分享功能的核心,是以详情页面为中心,一般博客针对文章的操作都很多,比如分享,点赞等,将这些链接都集中在详情页面,根据文章对象的不同字段来生成对应的URL,然后用其他视图函数在后边接着,根据需要实现各种功能。

这里有一点要注意的是,如果故意填错,某些新浏览器看不到后端验证错误的信息,而是会直接提示填写,这是因为新版浏览器针对表单字段如果在HTML5标签里设置了属性,就会进行浏览器验证,验证顺序是浏览器验证–>JS验证–>发送到后端验证。要关闭的话只要在FORM标签内加上novalidate即可,这是一个HTML5属性。

显示字段填写错误的页面如下:

功能2 建立评论系统

整理一下思路所谓评论系统,首先就想到,是在文章的详情页面展示所有的评论。肯定要设计一个评论页面供用户提交评论,之后添加到数据库中,然后再展示出来。其核心还是围绕详情页面生成一系列东西,然后用视图操作,最后返回详情页面。要做的事情有这些:

由于是评论,要新建一个评论ORM

页面上一片地方要填写评论,所以要建立评论表单

一个视图用于负责操作评论ORM

修改原来的详情页面,增加显示评论和提交评论的功能

建立评论ORM

没啥好多说的了,一开始肯定是评估所需的数据然后建立评论ORM,

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)

这次的外键是链接到文章上,因为一篇文章对应多个评论,和评论是一对多的关系,所以从多的那一方链到一的那一方。

现在可以详细解释一下related_name了。从Comment里找Post很简单,因为定义了post属性,要从Post里找对应的comments,如果没有定义related_name,则需要使用小写的comment类名加“__set”作为属性名,通过Post.comment__set来找所有的评论。远不如设置了related_name方便,设置了以后,只需要用Post.comment.all()就拿到了全部的评论组成的QuerySet。

一对多的查询官方文档在这里

这里还多了一个属性叫active,是因为评论一般有待审核或者不激活的状态,只有通过审核或者激活才会显示在文章下边,因此添加了这一个变量。这里设置了默认是True是为了方便,其实一般是默认为False,待通过审核后改成True,才会展示在页面上。

建立完了model,之后的工作就是migrate了,然后又是老套路,将该类注册到管理后台中去。

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

从模型建立表单

在发送邮件的功能里,采用继承forms.Form类,然后自行编写各个属性建立了一个表单。用户分享的时候没有必要将分享的内容保存到数据库中,所以这个表单没有对应的模型。但是现在不同了,这个表单直接对应到模型,所以可以从模型来直接建立对应的表单。

这次用到另外一个类 ModelForm,这个类使用方法很简单,如果不想做过多设置,很简单就能生成一个表单类:

from .models import Comment

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

继承ModelForm,然后在Meta类里指定模型名称是导入的Comment类,然后显式指定表单需要包含哪些字段,必须是Comment的属性名称。默认Django会生成全部字段。也可以用exclude = (field,…)来排除一些字段。

编写视图函数

由于这一切的操作都是在详情页面上发生了,所以将评论功能整合进详情功能中。由于默认的详情视图处理的是GET请求,而提交评论是POST请求,因此对视图进行一些改造:

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)
    # 列出这个post对应的所有活动的评论
    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.comments来拿到所有的评论,这就是利用了Comment类里的外键关联及设置的realated_name
  • 向一个页面传入变量的时候,比较不好的做法是视图函数根据不同的功能返回不同的参数,应该设置统一的传入参数和返回render,根据业务逻辑来决定所有参数的内容。
  • 这里学到了表单对象的新用法,就是.save(commit=False),即保存数据到对象中,但是不写入数据库,因为此时写入数据库的话,由于没有外键,数据库会报错。
  • 外键虽然在数据库内是一个链接,通过主键的外键实际上是一个数值,但是在ORM模型里,外键的值必须是一个数据对象,在当前页面评论的外键自然就链接到当前的文章上

编写模板

在编写模板的时候,先看一下视图函数传入的模板变量:

  • post 当前文章
  • comments 当前文章对应的所有评论
  • new_comment 这是成功添加的评论,如果没有成功添加,这个评论是None
  • comment_form 这是提交评论的表单

再多加一个需求,就是显示当前文章总的评论数。下边一个一个来实现

先添加所有评论的个数:这个可以通过.count来实现,在detail.html的content block部分添加:

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

这里要解释的新内容是:

  • with 是上下文变量,在这个with块内,就可以自定义变量的名称
  • |pluralize是根据数量来是否显示-s后缀的模板filter,很方便
  • ORM里计算拿到的QuerySet中查询结果的数量(行数)的函数用的是.count(),在模板内使用对象的方法时候,不能加括号。这种方式的局限性是不能操作带有强制传入参数的方法。如果有特殊需求,这个地方应该直接传入数值。

然后来编写展示所有评论的部分:

{% 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循环控制,即如果内容为空,则显示{% empty %}内的内容;如果不为空,就显示每条评论,在For循环内部可以用特殊的 forloop.counter来从1显示序号,非常方便。

最后是显示表单的部分,和之前的一样,要手动写form元素和加上sumbit功能:

{% 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不是None,就返回一个成功信息。默认情况下进来页面和添加不成功的情况下都是None,那么就显示表单。

完成的页面和展示评论的页面如下:


功能3 利用第三方库给文章添加标签

添加第三方库以及初步展示文章的标签

对于评论系统的完善,一般就可以文章加标签,这个可以通过第三方库 django-taggit来完成,提供了一个Tag类和管理器来给任何模型加标签。

使用需要先安装,通过pip来安装:

pip install django_taggit==0.22.2

这里要提一句,如果安装了django 2.1 版本的话,不要按照这里安装taggit的0.22.2版本,直接去安装最新版本,否则运行起来会有错误。由于教程一开始就推荐直接安装Django 2.1版,这里就遇到了插件版本低带来的问题。最新的taggit插件已经解决了该问题,使用方法和后文也没有任何区别。

这里安装指定版本,之后在setting.py里的INSTALLED_APPS 增加’taggit’,这样就添加了taggit作为一个APP,然后把taggit提供的模型管理器添加到Post类中去:

from taggit.managers import TaggableManager
    class Post....
        tags=TaggableManager()  # 从Taggit 导入的manager

这个管理器可以增删改查标签。现在我们的POST类有三个管理器了,分别是默认的objects,自定义专门取published状态的管理器,以及taggit的管理器。

现在模型已经改变,makemigration和migrate一下。

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

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

# 然后开始操作标签
post.tags.add("music","jazz","django")
post.tags.all()
# Out[8]: <QuerySet [<Tag: music>, <Tag: jazz>, <Tag: django>]>
post.tags.remove("jazz")
post.tags.all()
Out[10]: <QuerySet [<Tag: music>, <Tag: django>]>

可见很简单就插入了标签,其实能够想到,这个东西就是做了个外键关联,一个文章可以有多个标签,一个标签下边可以有多个文章,二者是多对多的关系。

在导入了Taggit之后,不用做配置,管理后台也能够看到Tag。现在还差在页面上展示,编辑list.html,在标题下边添加:

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

再刷新页面,就可以看到每个文章的tags了,这里利用了在模板中调用post.tags.all()的方法,然后用filter的join来连接各个tags标签的内容。

根据标签列出文章

标签的作用是让用户可以更快的找到感兴趣的内容,所以必须要设置列出一个标签下边的所有文章的功能。

其实能够想到的是,如果可以通过taggit类来迅速找到多对多里关联的文章就可以了。所以就要看一看tag类里是什么,然后用视图来操作即可。这里修改原来的post_list 视图,增加和tag相关的功能。

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])
    return render(request, 'blog/post/list.html', {'page': page, 'posts': posts,'tag':tag})

导入Tag类,然后设置一个tag为None,这是老手法了,即判断是不是有tag。修改了视图函数的参数,第二个位置放了个带默认值的关键字参数,后边肯定是要通过URL传进来。

这里的业务逻辑是如果tag_slug有值,则把之前的全部Post结果里再过滤一下,使用了tags__in的方式,必须在tag_slug里,所以tag_slug必须符合tags__in的要求。

由于多了tag这个变量,所以修改render,把这个变量也传进模板去。

之后来配置urls.py:

# 注释掉原来的CBV,还是改用FBV那一行:
path('', views.post_list, name='post_list'),
# 之后再添加一行,用于解析tag相关的URL
path('tag/<slug:tag_slug>/', views.post_list, name='post_list_by_tag'),

不要忘记在list.html里将include的上下文变量从CBV的 page_obj改成FBV的posts,再增加tag相关的内容:

{% include "pagination.html" with page=posts %}
{% if tag %}
<h2>Posts tagged with "{{ tag.name }}"</h2>
{% endif %}

然后把刚才写的展示每个文章的Tag部分修改成这样:

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

这个逻辑很简单,就是链接到指定标签的视图上去,然后返回该标签对应的全部文章。还记得之前编写的视图吗,如果tag为None,就是全部文章,然后展示每个文章的标签。如果tag不是none,就从全部的文章里取等于这个tag的文章,再展示。

现在启动服务,就可以发现,满足了要求。页面长这个样子:

通过标签相似性推荐文章

通过标签相似性推荐文章指的是用户在浏览文章的时候,可以在页面内推荐具有相同标签的文章,同时具备越多的标签相似性越好。为了达到目的,需要做这么几步:

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

这个功能就使用到了复杂的QuerySet,而且推荐文章是在用户浏览文章的时候推荐,所以显示在detail.html,涉及到的视图函数是post_detail,这里还需要使用Django ORM里用于提供数据库的聚合查询函数功能的四个类

from django.db.models import Count, Avg, Sum Min, Max

这五个函数对应数据库的聚合查询的计数,平均数,求和,最小值和最大值。而且引入了聚合函数,说明ORM里我们开始使用相当于数据库里的分组功能了。

聚合查询功能的官方文档在这里

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

# 显示相近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]
# 修改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})

这里用到了几个东西:

  • values_list方法,前边的几个方法返回QuerySet,这个方法返回一个列表,每个列表里是元组,放着每一行数据类似字典,后边参数的id表示结果只包含该字段,flat=True表示不按照每个对象排列,而是直接生成一个列表包含每个id。这些内置方法用起来都非常方便。
  • 拿到了当前文章的全部tags对应的id号,下一步就是从文章列表里,将这个列表里所有的tag对应的文章全部找出来,但是不包含这个文章自己。
  • 最后一步是引入了分组,.annotate方法就是分组,指定一个新的字段名,用于计算tags出现的次数,然后按照这个次数降序排列之后按照发布时间降序排列。
  • 这里还学到了order_by方法可以有多个参数,按照次序进行排列。

这里要解释的是最后一句话,similar_tags变量里边,其实是一条条Post记录,这其中可能会有重复的记录,只要有两个或以上标签和当前文章相同。分组做的就是按照tags字段计数,之后排序,similar_posts就类似下边:

id same_tags publish ……
3 3 …… ……
18 2 …… ……
15 1 …… ……

然后要做的事情,就是将similar_posts这个对象传递到模板中显示出来。

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

修改后的详情页面如下:

功能实现了。其实taggit模块中有一个similar_objects()的模型管理器也可以实现这个功能,可以看taggit的文档自己折腾。

到这里给博客添加的一些高级功能都实现了,其实还可以从很多细节继续强化功能,比如在详情页添加标签,点标签跳转到该详情页列出的所有post,只需要把list里的那段代码拿过来即可。

总结

这次给博客添加了一些新功能,主要用到了以下知识点:

django内发送邮件的配置,以及使用方法

使用Form类建立标准表单和使用ModelForm从模型中直接建立表单

表单字段的关键字参数设置与表单验证和页面表现形式之间的关系

urls.py写上name,然后在模板中用{% url %}进行反向解析,如果是正则字符串,则要传对应的参数进去。

{% for %}循环里的{% empty %}和forloop变量

第三方库taggit的使用

ORM分组查询和聚合函数的使用,value_list与flat控制。