BLOG
Enjoy when you can, and endure when you must.
APR 13, 2016/Django
用 Django 构建简易博客(四):专注功能的实现之博客详情与评论

在我建立这个网站之初,我就发了一个系列的博客《Django 博客系统开发》,当时的想法是将自己所学和所实践的一些东西整理一下、记录下来。时至今日,三年的时光已在眨眼间过去,我发现这几篇文章在我网站的访问量排行中依然居高不下。说明这几年大家对 Python 和 Django 的关注度确实比较高并且有很多新的开发者加入其中,这当然要数是一个非常好的趋势。但技术是不断发展的,特别 Python 和 Django 都一直处于快速发展期,当年的文章中提到的方法很多已不再适用。因此萌发了做一个更新的想法,让更多的朋友关注最新的技术,而不是面对一个旧版冥思苦想。

用 Django 来快速搭建一个简易的博客,我一直认为是一个自我实践的好方法。就像我自己的网站一样,我在多个版本迭代中出现了一个从简到繁、又从繁到简的过程,这是我不断的在尝试和优化。只要愿意关注细节、乐意去思考,即使是一个很小的东西,也能从中收获不少。

当然,这里我用到了一个名词:实践。是的,要想做出一样东西,即使再简单,也需要基础来支撑。因此在开始实践前,务必要首先学习 Python 语言本身并对 Django 开发框架有一定的了解。
这也明确了本文所面向的读者:具备 Python 基础,对 Django 开发框架有一定了解,想要利用他来做一些基础实践的开发者。如果你还处于零基础阶段,建议阅读一下 Mark Lutz 的《Learning Python》以及 《The Django Book》两本书籍,他们能够引领你进入这个奇妙的世界。

另外,文章中会包含一些我个人的编码风格、见解与主张。因此并不一定要遵循。如果对其有一些独到的看法和改进意见,我们都可以随时沟通、共同改进。

 

第四篇:专注功能的实现之博客详情与评论功能

 

上一篇中,我们已经成功实现了“博客列表”页。可以看出,在 Django 中,要想实现一个最基本的功能是非常容易的,只需时刻套用 MTV 这个概念,即可很快构建出想要的功能。现在我们要将目标聚焦到“博客详情”页上,首先还是来回顾一下需求:

需要有一个“详情”页来呈现完整的博客,包括标题、作者、分类、发布的时间、标签和完整的正文内容,并附加评论显示和发布功能。

可以将以上需求这么来分解:

  • 页面构造:包括博客正文及评论的展示。另外还需要为用户评论创建一个表单以方便内容的输入和提交;
  • 用户评论:对评论的提交并将用户引导回之前浏览的页面。

这样就把问题分解为了两部分,接下来就来分别关注。

 

“博客详情”页的构造

有之前实现“博客列表”的基础之后,这个任务对于我们来说应该已经不在话下了。
首先来进行一个简单的分析:这里因为针对的是一篇博客,所以必须要将其找出来,这就需要有一个唯一的标识。好在 Django 的模型中确实默认就有这么一个唯一的且未自增长的主键,即 id 字段。

我们可以以此为关键字来定位一篇博客。那还有一个问题就是如何才能收到该关键字呢?这时就要利用到带通配符的 URL 定义并使用圆括号把参数在 URL 模式里标识出来,而标识出来的内容就会以参数的形式传入到视图函数中。比如这样:

(r'^detail/(\d+)/$', get_detail, name='blog_get_detail'),

圆括号中匹配到的数字就会传入到 detail 的第二个参数中去(还记得吗?第一个参数始终为 request 对象)。

如何定位一篇博客的问题解决了,还有另一个问题摆在我们面前:如何生成和处理用户的评论?这应当很快联想到 Django Form 能帮助我们快速实现这一需求。因此目前的任务就是来构建一个表单。新建一个文件 blog/forms.py,并写入以下代码:

from django import forms


class CommentForm(forms.Form):
    """
    评论表单
    """

    name = forms.CharField(label='称呼', max_length=16, error_messages={
        'required': '请填写您的称呼',
        'max_length': '称呼太长'
    })

    email = forms.EmailField(label='邮箱', error_messages={
        'required': '请填写您的邮箱',
        'invalid': '邮箱格式不正确'
    })

    content = forms.CharField(label='评论内容', error_messages={
        'required': '请填写您的评论内容',
        'max_length': '评论内容太长'
    })

代码中定义了一个评论表单的类并根据需求定义了三个字段:称呼、邮箱和评论内容。这样我们就能利用它来快速生成表单并验证用户的输入。
到此几个小屏障都扫清了,可以开始编写视图函数了:

from django.http import Http404
from .forms import CommentForm


def get_detail(request, blog_id):
    try:
        blog = Blog.objects.get(id=blog_id)
    except Blog.DoesNotExist:
        raise Http404

    if request.method == 'GET':
        form = CommentForm()
    else:
        form = CommentForm(request.POST)
        if form.is_valid():
            cleaned_data = form.cleaned_data
            cleaned_data['blog'] = blog
            Comment.objects.create(**cleaned_data)

    ctx = {
        'blog': blog,
        'comments': blog.comment_set.all().order_by('-created'),
        'form': form
    }
    return render(request, 'blog-detail.html', ctx)

该视图函数要比之前的列表函数复杂一些,我们以代码中的空行为界来对其进行拆分并分析:

  • get_detail() 视图函数首先利用传入的 blog_id 到数据库中查询该条博客记录。注意这使用了 try 块,因为 model.objects.get() 方法会在未能查询到数据的情况下抛出 model.DoesNotExist 的异常。如果没有对此异常进行拦截的话,就会导致服务器 500 错误,这显然不是用户想看到的。而是应该在发生此错误的情况下通知用户“你要访问的内容未能找到”。一般情况下网站都会在这个时候向用户抛出一个 404 错误并定义一些“生动有趣”的话语来提示用户同时又不会显得太突兀而让用户产生反感;
  • 中间一个部分是对表单的初始化和校验处理。这里会涉及到 HTTP 的 GET 请求和 POST 请求。一般来说,GET 请求用于获取数据,而 POST 请求则为提交数据。这里也遵循此一般约定,在用户做 GET 请求时,仅初始化一个空表单供用户填写;而如果是 POST 请求的话,则在初始化的同时将用户传入的数据传入。用户传入的数据会收集在 request.POST 中,是一个类似于字典的对象。接下来是调用 form 类的 is_valid() 方法来对用户输入做校验,如果校验成功,则创建一条评论记录;
  • ctx 依然是要传入到模板的上下文参数,其中 blog 是博客对象,comments 是利用 ORM 的反查方法找到当前博客包含的所有评论,并以发布时间的倒序方式进行排列,另外还有一个 form,是之前定义的 CommentForm 的实例化对象,它用于初始化评论表单。

最后来定义模板。先来看代码,新建 templates/blog-detail.html 并写入如下内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ blog.title }}</title>
    <style>
        .blog {
            padding: 20px 0px;
        }
        .blog .info span {
            padding-right: 10px;
        }
        .blog .summary {
            padding-top: 20px;
        }
    </style>
</head>
<body>

<div class="header">
    <span><a href="{% url 'blog_get_blogs' %}">博客</a> - <a href="{% url 'blog_get_detail' blog.id %}">{{ blog.title }}</a></span>
</div>

<div class="content">
    <div class="blog">
        <div class="title">
            <a href="#"><h2>{{ blog.title }}</h2></a>
        </div>
        <div class="info">
            <span class="category" style="color: #ff9900;">{{ blog.category.name }}</span>
            <span class="author" style="color: #4a86e8">{{ blog.author }}</span>
            <span class="created" style="color: #6aa84f">{{ blog.created|date:"Y-m-d H:i" }}</span>
        </div>
        <div class="summary">
            {{ blog.content }}
        </div>
    </div>
    <div class="comment">
        <div class="comments-display" style="padding-top: 20px;">
            <h3>评论</h3>
            {% for comment in comments %}
                <div class="comment-field" style="padding-top: 10px;">
                    {{ comment.name }} 说: {{ comment.content }}
                </div>
            {% endfor %}
        </div>
        <div class="comment-post" style="padding-top: 20px;">
            <h3>提交评论</h3>
            <form action="{% url 'blog_get_detail' blog.id %}" method="post">
                {% csrf_token %}
                {% for field in form %}
                    <div class="input-field" style="padding-top: 10px">
                        {{ field.label }}: {{ field }}
                    </div>
                    <div class="error" style="color: red;">
                        {{ field.errors }}
                    </div>
                {% endfor %}
                <button type="submit" style="margin-top: 10px">提交</button>
            </form>
        </div>
    </div>
</div>

</body>
</html>

这里对几个新出现的模板语言/标签进行一个简要的解释:

  • {% url 'blog_get_blogs' %} 可以看作是 reverse 方法的“模板语言”版,其作用是根据 URLConf 中的 name 定义对 url 进行反解析,转换成真实的 URL 地址。比如这里在转换之后会变成 “/”。如果所指定的 url 定义中包含参数,则需要将参数跟在后面,如 {% url 'blog_get_detail' blog.id %};
  • 观察表单的内容,{% csrf_token %} 用于防跨域请求,可参考一些关于 CSRF 相关的资料并阅读 Django 官方文档关于这一块实现的描述。{% for field in form %}{% endfor %} 是对表单的各个 field 进行迭代并生成相应的表单元素,并对校验过程中出现的错误进行显示以提示用户做相应的修改。最后还生成一个 button 用于点击提交评论。

来看看最终的效果:

最后,记得将 template/blog-list.html 中博客标题上 a 标签中的 href 也改为 {% url 'blog_get_detail' blog.id %},当用户点击标题时就可以直接进入详情页面了。

到目前为止,我们已经达到了之前提到的所有需求。感觉如何呢?

 

发现更多

 

让表单提交更具风格

 

在之前的例子中,点击表单的提交按钮之后,整个页面是会重新加载的,这会让页面交互显得不那么自然。在现代网页中,多数表单交互都是无需整体刷新的,用户点击提交之后,会出现一个“正在提交”的提示,在完成之后再次提示用户“已提交成功”。而在此过程中,用户可以随心所欲的做一些别的事情,比如再揣摩一下你的博客内容。这就要用到所谓的 Ajax,去了解一下,然后让你的表单更有范儿吧!

 

多级评论

 

你可能需要一个多级评论来支持类似于评论回复的功能。有思路吗?可以这样思考:回复一个评论实际就是将一个评论挂到另一个评论之下。好了,快试试去实现它。

 

继续阅读下一篇《用 Django 构建简易博客(五):添枝加叶

COMMENTS
02/09From Edison

type object 'comment' has no attribute 'objects'
大佬,一直报这个错误 ,怎么解决啊??

错误文件: File "F:\Python\PyCharmWork\iBlogSystem\blog\views.py", line 29, in get_page
Comment.objects.create(**cleaned_data)
NameError: name 'Comment' is not defined

12/06From lzk

我复制过去为什么出现这个
manager_method() missing 1 required positional argument: 'self'

28/03From panyanghe

感谢

17/03From 虫子

请问博主,评论提交以后表格不清空啊

31/01From Alan

博主,为什么我老是提示“Blog object has no attribute 'comment_set'”?

16/12From wosabi

02/11From James

Very good Thanks U

18/07From Wikey.Zhang

博主666,感谢您的教程

04/07From 四大皆空法搜电风扇里面啥都没分类考试范围十分舒服就死哦福建师范

师傅师傅晚上v

25/05From lile

测试

25/05From lile

你好

03/02From Danny

@DwD: {{ field.errors }} 是在表单填写错误的时候才会有内容的,且会在页面中渲染出来。error_messages 的定义不同类型的 Field 有所不同,可看一下 Django 官方文档对于表单 Field 的介绍(https://docs.djangoproject.com/en/1.10/ref/forms/fields/),里面都注明了某个 Field 会有哪些类型的错误(Error message keys)。

01/02From DwD

博主,请问一下,
<div class="error" style="color: red;">
{{ field.errors }}
</div>
这一段好像没作用?
还有,在forms.py里面定义的error_messages怎么用呀?

11/01From Jimmy

博主你好,我现在有一个问题就是,为什么我的评论表单内容提交后,方框里面的文字没有刷新掉,而且手动刷新页面会重复提交内容呢?

04/01From AlexWang

if form.is_valid(): is_valid方法是不是没有定义

02/12From jimmy

谢谢楼主,我明白啦,您的博客对我启发很大,再次谢谢楼主

08/11From lamont

请问视图函数中 'comments': blog.comment_set.all().order_by('-created') 这句怎么理解,
没看懂 blog.comment_set 的含义 。

07/11From wangxian

<a href="{% url 'blog_get_detail' blog.id %} 这个应该是修改blog-detail.html里面的吧 还有URL怎么定义啊 一直没想通

06/11From Danny

@Jimmy: Model.objects.create() 方法需要 k-v 参数,k 为 Model 中对应的 field,v 为所要设定的值。我在 form 中所定义的字段与 Model 中的字段是对应的,而 cleaned_data['blog'] = blog 是为了把评论和 Blog 对应起来。不知这么说能否有所启发。

03/11From Jimmy

cleaned_data = form.cleaned_data
cleaned_data['blog'] = blog
Comment.objects.create(**cleaned_data)
没看懂这个什么意思呢,博主能解释一下吗,谢谢

26/10From lgh

@mxy:应该是comment没加s所以读不出来,我也是同样的问题,后来发现cts里面comment没加s

29/09From abc111

有点不理解表单的验证过程

05/08From 真的不错

真的很不错

01/08From Danny

@mxy: 如果可以的话,提供一下你的测试代码。

01/08From mxy

博主您好有个问题想请教您一下,为什么我按照你的代码写了测试的时候一点发表页面不刷新,而且评论里面不添加内容

26/06From craymc

Reverse for 'blog_get_detail' with arguments '(1,)' and keyword arguments '{}' not found. 0 pattern(

21/06From 啦啦啦

感谢博主教程 关于总是提示NameError: global name 'Comment' is not defined这个错误

先添加 from .models import Comment

08/06From Danny

@zj: 要在 views.py 中导入一下 Comment,即“评论”的模型。

07/06From zj

总是提示NameError: global name 'Comment' is not defined

03/06From Danny

@秀: 非常感谢!的确是我的疏忽。我已对问题进行修正。

03/06From 秀

{% url 'blog_get_detail' blog.id %}两边要有英文双引号
url里面要有 name='blog_get_detail'

03/06From 秀

小错误修正:
blog-list.html:把 <a href='#'>修改为<a href="{% url 'blog_get_detail' blog.id %}">
同时,在urls.py的

03/06From 有个小问题

感谢博主的教程,{% url 'blog_get_detail' blog.id %} 这里老是出错是为什么?

29/05From Danny

@123456: 问题是出在 urls.py 中吗?

29/05From 123456

博主你好 我照您的演示在网页显示出name 'detail' is not defined 试了好多方法也没用求指教

24/05From lyl

感谢您的教程

LEAVE COMMNT