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

作者: lakerszhy | 来源:发表于2017-04-27 14:09 被阅读623次

    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源,并在应用中集成一个高级的搜索引擎。

    相关文章

      网友评论

        本文标题:第二章 为博客添加高级功能

        本文链接:https://www.haomeiwen.com/subject/tangzttx.html