Django搭建个人博客:在博文中发表评论

2212阅读 · 15评论 · 2019/01/02发布   前往评论

在没有互联网的年代,我们用日记来记录每天的心得体会。小的时候我有一个带锁的日记本,生怕被别人看见里面写了啥,钥匙藏得那叫一个绝。

现在时代变了,网络版的日记本:博客,却巴不得越多人看越好。

别人看完你写的深度好文,难免也想高谈阔论一番,这就是“评论”功能。

本章将要编写的评论模块,几乎没有新的知识点,而是将前面章节内容的综合应用。

强烈建议读者自行尝试编写这部分内容,测试自己的知识掌握程度。

准备工作

评论是一个相对独立的功能,因此新建一个评论的app:

(env) E:\django_project\my_blog > ppython manage.py startapp comment

有的人觉得奇怪,没有博文就没有评论,为什么说评论是“独立”的功能?

那是因为不仅博文可以评论,照片、视频甚至网站本身都可以“被评论”。将其封装成单独的模块方便以后的扩展。

确认app创建成功后,记得在settings.py中注册:

my_blog/settings.py

...
INSTALLED_APPS = [
    ...
    'comment',
]
...

TIME_ZONE = 'Asia/Shanghai'

...

因为我们想显示发表评论的时间,修改时区设置TIME_ZONE为上海的时区。

然后在my_blog/urls.py中注册根路由:

my_blog/urls.py

...
urlpatterns = [
    ...
    # 评论
    path('comment/', include('comment.urls', namespace='comment')),
]
...

编写核心功能

评论的模型

首先编写评论的模型:

comment/models.py

from django.db import models
from django.contrib.auth.models import User
from article.models import ArticlePost

# 博文的评论
class Comment(models.Model):
    article = models.ForeignKey(
        ArticlePost,
        on_delete=models.CASCADE,
        related_name='comments'
    )
    user = models.ForeignKey(
        User, 
        on_delete=models.CASCADE, 
        related_name='comments'
    )
    body = models.TextField()
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ('created',)

    def __str__(self):
        return self.body[:20]

模型中共有2个外键:

  • article是被评论的文章
  • user是评论的发布者

别忘了每次新增、修改Model后,必须数据迁移

提示:你必须先在setting.py中注册app,这个app中的数据迁移才能生效

评论的表单

用户提交评论时会用到表单,因此新建表单类:

comment/forms.py

from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['body']

因为模型中的2个外键将通过视图逻辑自动填写,所以这里只需要提交body就足够了。

评论的url

在comment app中新建路由文件:

comment/urls.py

from django.urls import path
from . import views

app_name = 'comment'

urlpatterns = [
    # 发表评论
    path('post-comment/<int:article_id>/', views.post_comment, name='post_comment'),
]

评论必须关联在某篇具体的博文里,因此传入博文的id,方便后续调用。

post_comment()视图还没写,先取个名字占位置。

评论的视图

评论的视图函数如下:

comment/views.py

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse

from article.models import ArticlePost
from .forms import CommentForm

# 文章评论
@login_required(login_url='/userprofile/login/')
def post_comment(request, article_id):
    article = get_object_or_404(ArticlePost, id=article_id)

    # 处理 POST 请求
    if request.method == 'POST':
        comment_form = CommentForm(request.POST)
        if comment_form.is_valid():
            new_comment = comment_form.save(commit=False)
            new_comment.article = article
            new_comment.user = request.user
            new_comment.save()
            return redirect(article)
        else:
            return HttpResponse("表单内容有误,请重新填写。")
    # 处理错误请求
    else:
        return HttpResponse("发表评论仅接受POST请求。")

代码中有2个新面孔。

get_object_or_404():它和Model.objects.get()的功能基本是相同的。区别是在生产环境下,如果用户请求一个不存在的对象时,Model.objects.get()会返回Error 500(服务器内部错误),而get_object_or_404()会返回Error 404。相比之下,返回404错误更加的准确。

redirect():返回到一个适当的url中:即用户发送评论后,重新定向到文章详情页面。当其参数是一个Model对象时,会自动调用这个Model对象的get_absolute_url()方法。因此接下来马上修改article的模型。

实际上之前的章节已经用过redirect()了。功能是相同的,实现上略有区别。

文章的模型

按照上面说的,在文章模型中添加get_absolute_url()方法:

article/models.py

...
# 记得导入
from django.urls import reverse

class ArticlePost(models.Model):
    ...

    # 获取文章地址
    def get_absolute_url(self):
        return reverse('article:article_detail', args=[self.id])

通过reverse()方法返回文章详情页面的url,实现了路由重定向。

文章详情视图

评论模块需要在文章详情页面展示,所以必须把评论模块的上下文也传递到模板中。

因此修改article/views.py中的article_detail()

article/views.py

...

from comment.models import Comment

def article_detail(request, id):
    # 已有代码
    article = ArticlePost.objects.get(id=id)

    # 取出文章评论
    comments = Comment.objects.filter(article=id)
    ...

    # 添加comments上下文
    context = { 'article': article, 'toc': md.toc, 'comments': comments }

    ...

filter()可以取出多个满足条件的对象,而get()只能取出1个,注意区分使用

文章详情模板

到最后一步了,坚持。所有后台的功能已经写完了,就差把所有这些展现到页面中了。

修改文章详情页面:

templates/article/detail.html

...

<div class="col-9">
    ...
    <!-- 已有代码,文章正文 -->
    <div class="col-12">
        ...
    </div>

    <!-- 发表评论 -->
    <hr>
    {% if user.is_authenticated %}
        <div>
            <form 
                action="{% url 'comment:post_comment' article.id %}" 
                method="POST"
            >
            {% csrf_token %}
                <div class="form-group">
                    <label for="body">
                        <strong>
                            我也要发言:
                        </strong>
                    </label>
                    <textarea 
                        type="text" 
                        class="form-control" 
                        id="body" 
                        name="body" 
                        rows="2"></textarea>
                </div>
                <!-- 提交按钮 -->
                <button type="submit" class="btn btn-primary ">发送</button>                    
            </form>
        </div>
        <br>
    {% else %}
        <br>
        <h5 class="row justify-content-center"><a href="{% url 'userprofile:login' %}">登录</a>后回复
        </h5>
        <br>
    {% endif %}



    <!-- 显示评论 -->
    <h4>共有{{ comments.count }}条评论</h4>
    <div>
        {% for comment in comments %}
            <hr>
            <p>
                <strong style="color: pink">
                    {{ comment.user }}
                </strong><span style="color: green">
                    {{ comment.created|date:"Y-m-d H:i:s" }}
                </span> 时说:
            </p>
            <pre style="font-family: inherit; font-size: 1em;">
{{ comment.body }}</pre>
        {% endfor %}
    </div>
</div>

<!-- 目录 -->
<div class="col-3 mt-4">
    ...
</div>

...
  • 表单组件中的action指定数据提交到哪个url中

  • 显示评论中的comments.count是模板对象中内置的方法,对包含的元素进行计数

  • |date:"Y-m-d H:i :s":管道符你已经很熟悉了,用于给对象“粘贴”某些属性或功能。这里用于格式化日期的显示方式。请尝试修改其中的某些字符试试效果。

  • <pre>定义预格式化的文本,在我们的项目中最关键的作用是保留空格和换行符。该标签会改变文字的字体、大小等,因此用style属性重新定义相关内容。尝试将<pre>替换为div,输入多行文本试试效果。

之前说代码最好不要复制粘贴,否则有些“小坑”你是留意不到的。比如在<pre>标签中的文本千万不能缩进。

测试

又到了激动人心的测试环节了。

登录自己的账户,进入某个文章详情页面,发现已经可以进行留言了:

如果退出登录,显示提示语:

点击登录就回到登录页面。

评论模块的发布、展示功能就搞定了。

扫尾工作

数据的删、改功能我们已经做过很多遍,这里不打算再赘述了。

评论同样也可以支持Markdown语法,或者插入Emoji表情符号。

读者可以自己去实现感兴趣的功能。

有些网站干脆就没有删除、更新评论的功能。因为对小站来说,这些功能用到的次数太少太少了,不如把精力用在更有价值的地方去。比如我的博客就没有。

还有的网站提供软删除,删除后仅仅是不显示而已,实际上数据还存在。

具体应该如何做,都以你的喜好而定。

总结

本章实现了发表评论、展示评论的功能。像开头说的一样,本章的内容是前面所学章节的综合。

如果你没有看本章代码,而是完全靠自己完成了评论功能,那么恭喜你获得了“Django入门程序员”的称号!不要小看“入门”两字,万事开头难嘛。





<< 文章目录 结束和开始 >>
本文作者: 杜赛
发布时间: 2019年01月02日 - 10:25
最后更新: 2019年01月28日 - 22:31
知识共享许可协议   转载请保留原文链接及作者

登录 后回复

共有15条评论

avatar
河底客 么么哒! 2

老师你好,我尝试编写删除评论的功能,但是不知道如何查询出具体评论,问题代码如下:

def delete_comment(request,id): 
    """删除评论""" 
    comment =Comment.objects.get(id=id) comment.delete() 
    return HttpResponseRedirect(reverse('main_content:article_detail',args=[article.id])) 

报错也很明显,就是Comment matching query does not exist.评论匹配的查询集不存在,我也意识到是跟外键关联的文章有关系,但在视图函数里要怎么写呢?谢谢

6个月前 回复


avatar
杜赛 [博主] 河底客 么么哒! 1

模板传递过来的id对了吗?

print(id)打印出来,在命令行里看一下呢。

结合sqlitestudio中的数据进行对比

6个月前 回复


avatar
河底客 杜赛 [博主] 么么哒! 1

模板传来的id不对,模板传来的是文章自身的article.id,然后我将其改成了"{% url 'comments:delete_comment' comment.id %}" 后,直接连文章详情页整个报错了,报错为:everse for 'delete_comment' with arguments '('',)' not found. 1 pattern(s) tried: ['article_comment/(?P[0-9]+)/$'] 请问如何才能传递正确的评论自身的ID呢?

6个月前 回复


avatar
杜赛 [博主] 河底客 么么哒! 1

你这个报错说明id还是没有传递进来。

你的评论是用{% for comment in comments%}来循环渲染的吧?

注意你删除评论的url要写在for循环体里面哦。否则是取不到值的。

你试试。 如果还不行,就把项目打包发到我邮箱里,我帮你改改。最快明天晚上有空帮你看。

6个月前 回复


avatar
河底客 杜赛 [博主] 么么哒! 1

谢谢,就是这个原因!

但是我把删除的弹窗确认js函数放在模板for循环的时候,删除有些异常,具体表现为,我按顺序有1、2、3条评论,那么我删除3的时候,消失的评论并不是3,而是1,再删除3,消失的依然不是3.而是2,好像是按顺序来删的,不是按id

不过去除确认删除的js函数就没问题,直接用a标签免确认就可以直接删除指定评论,没有问题了。

6个月前 回复


avatar
杜赛 [博主] 河底客 么么哒! 1

好的,搞定了就好。

简单问题博客里留言,复杂的发Email给我

6个月前 回复


avatar
SeaZhusp 么么哒! 1

点个赞,评论后为什么会跳回到顶端

3个月前 回复


avatar
杜赛 [博主] SeaZhusp 么么哒! 1

感谢。评论后应该是定位在你的新评论上的。我这边调试是正常的,可能哪里有点问题吧。

3个月前 回复


avatar
ac1864 么么哒! 1

过关。

老大的教程真不错,

github的代码的branches设置也很用心。

1个月前 回复


avatar
Nl-Lin 么么哒! 1
本回复已被 Nl-Lin 删除

1个月前 回复


avatar
Nl-Lin Nl-Lin 么么哒! 1
本回复已被 Nl-Lin 删除

1个月前 回复


avatar
shenhanlin 么么哒! 1

博主你好,代码没有问题,就是为什么提交评论之后,没有跳转到该评论所处的位置,而是提交评论后,跳转至了页面的顶端?我反复看了代码也没有解释说明那个地方的代码是提交评论后,页面刷新会跳转至该评论所处的位置。

1个月前 回复


avatar
杜赛 [博主] shenhanlin 么么哒! 1

这个章节并没有讲你说的功能。

请参阅后面的锚点定位的章节。

1个月前 回复


avatar
shenhanlin 杜赛 [博主] 么么哒! 1

嗯好的,谢谢博主

1个月前 回复


avatar
CearlERO 么么哒! 1
本回复已被 CearlERO 删除

5天前 回复