使用django-mptt与django-comments制作评论功能

原本自己写的评论模块功能很简单,不能实现现在很常见的多层评论功能。于是决定把它重构一下。 使用的是django-comments和django-mptt两个包。 django-comments是django官方制作的库,用于实现评论功能,原本集成在django内,后来分离了出来。 django-mptt是一个提供树状结构模型的库。

安装与配置

首先安装这两个模块

$ pip install django-comments
$ pip install django-mptt

在settings的installed-apps中加入django-comments, mptt以及django-comments依赖的site包

INSTALLED_APPS = [
    site,
    django-comments,
    mptt,
]

创建评论app

$ python manage.py startapp comments

settings中设置参数

SITE_ID = 1 # django-comments依赖site,需要设定STIE_ID
COMMENTS_APP = 'comments'   # 指定django-comments使用我们自己写的模块

模型设定

模块建好了,首先打开model.py写评论模型

comments/model.py文件内容

from django.core.cache import cache
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django_comments.abstracts import CommentAbstractModel
from django_comments.managers import CommentManager
from mptt.managers import TreeManager
from mptt.models import MPTTModel, TreeForeignKey
from mptt.querysets import TreeQuerySet

from utils.rich_content import generate_rich_content


class BlogCommentQuerySet(TreeQuerySet):
    # 自定义查询集,只查询公开未删除的评论
    def visible(self):
        return self.filter(is_public=True, is_removed=False)

    def roots(self):
        return self.visible().filter(parent__isnull=True)


class BlogCommentManager(TreeManager, CommentManager):
    # PostComment模型继承于MPTTModel和 CommentAbstractModel,它的Manager也要继承于这两者的Manager
    pass


class PostComment(MPTTModel, CommentAbstractModel):

    # 时间信息
    create_time = models.DateTimeField(_('创建时间'), default=timezone.now)

    # 层级关系
    parent = TreeForeignKey('self', verbose_name=_('父评论'), null=True, blank=True,
                            on_delete=models.DO_NOTHING, related_name='children')

    objects = BlogCommentManager.from_queryset(BlogCommentQuerySet)()

    class Meta(CommentAbstractModel.Meta):
        verbose_name = _("comment")
        verbose_name_plural = _("comments")
        db_table = 'Blog_comments'

    class MPTTMeta:
        order_insertion_by = ["-submit_date", "user_id"]

    def __str__(self):
        return f'{self.user}:{self.comment[:40]}'

    @property
    def comment_html(self):
        return self.rich_content.get("content", "")

    @cached_property
    def rich_content(self):
        ud = self.submit_date.strftime("%Y%m%d%H%M%S")
        md_key = 'comment{}_md_{}'.format(self.id, ud)
        cache_md = cache.get(md_key)
        if cache_md:
            rich_content = cache_md
        else:
            rich_content = generate_rich_content(self.comment)
            cache.set(md_key, rich_content, 60*60*12)
        return rich_content

这里的评论模型多重继承了mptt和comments中的MPTTModel, CommentAbstractModel。mptt内置了树结构,comments模型内置了大多数评论的属性,像账号关联、评论内容、评论对象等等。我打算做成发布后还可以进行修改,显示按创建时间排序、不使用修改时间submit_date所以加入了创建时间create_time属性,parent和objects是mptt需要设定的属性,parent用于关联父评论。 在class Meta中设置显示的名字和数据库表名

属性方法rich_content获得经过markdown渲染的评论内容,这里加入了缓存机制使得不需要每次打开网页都要渲染一次。改写__str__使模型的str显示更直观。

在comments模块的__init__.py文件中写入

def get_model():
    from comments.models import PostComment
    return PostComment

指定django-comments模块使用的模型,django-comments会执行get_model()函数寻找使用的评论模型,如果在指定自定义模块之中没有找到,将执行内置的get_model()函数指向内置的模型。

表单

django-comments内置了提交评论的表单,但这不足满足我们的需要,所以需要重写。 新建comments/form.py

from django import forms
from django_comments.forms import CommentForm
from comments import get_model


class PostCommentForm(CommentForm):

    parent = forms.IntegerField(required=False, widget=forms.HiddenInput)

    def __init__(self, target_object, data=None, initial=None, parent=None, **kwargs):
        self.user = kwargs.pop('user', None)
        self.parent = parent
        if initial is None:
            initial = {}
        if parent:
            initial.update({'parent': self.parent})
        super().__init__(target_object, data=data, initial=initial, **kwargs)

    def get_comment_model(self):
        return get_model()

    def get_comment_create_data(self, **kwargs):
        data = super().get_comment_create_data(**kwargs)
        parent = self.cleaned_data.get('parent')
        data['parent_id'] = parent
        return data

我们继承django-comments中的表单,额外加入隐藏项输入项parent用于关联父评。 重写初始化方法,向表单传入userparent两个属性。 与模型类似也要在comments模块的__init__.py文件中写入get_form()函数让comments模块使用我们自定义的表单。

def get_form():
    from comments.forms import PostCommentForm
    return PostCommentForm

标签模板

django-comments使用tag的形式将评论加入页面中。 render_list_for用于生成一个对象的所有评论的列表。因为额外加入了树状结构,这个tag无法直接使用。需要自己自定义它们的模板。 render_form_for用于生成对一个对象的评论框。

django-comments会自动在templates/comments/目录中寻找模板,没有则使用内置默认模板。我们把django-comments包中的内置默认模板复制到自己的templates/comments中再自己按需要修改。

render_form_forrender_list_for分别对应的是form.htmllist.html这两个文件。form按自己喜欢修改样式就好了。主要关注list.html多层评论的显示就在这里面。

list.html

{% load comments %}
{% load comments_extras %}
{% load mptt_tags %}
{% load i18n %}
{% load static %}
{% get_comment_list for post as comment_list %}
<dl class="comment-list list-unstyled" id="comments">
    {% recursetree comment_list %}
    <!-- MPTT的树状显示的标签 -->
        <dt class="comment-item" id="c{{ node.id }}">
            <span class="username">{{ node.user.username }}</span>
            <time class="create_time" datetime="{{ node.create_time }}">{{ node.create_time }}</time>

            {% if node.parent %}<span>{% trans "reply" %}{{ node.parent }}</span>{% endif %}
            <div class="text">
                {{ node.comment_html|safe }}
            </div>
        </dt>

        {% if not node.is_leaf_node %}
        <!-- 本节点有子节点则在下方显示其子节点 -->
            <dl class="children" style="margin-left: 40px">
                {{ children }}
            </dl>
        {% endif %}

    {% endrecursetree %}
</dl>

效果如下:

评论效果

回复评论

现在对文章的评论只要使用{% render_form_for_post %}就可以了,但是要回复评论使用{% render_form_for_postcomment %}还是不行,缺少了参数。所以还要写一个view传入要回复的评论parent。

comments/view.py

class ReplyView(FormMixin, DetailView):
    model = PostComment
    form_class = PostCommentForm
    pk_url_kwarg = 'parent'
    template_name = 'comments/form.html'

    def get_form_kwargs(self):
        kwargs = super(ReplyView, self).get_form_kwargs()
        kwargs.update({
            'target_object': self.object.content_object,
            'parent': self.object.pk,
            'user': self.request.user
        })
        return kwargs

comments/url.py

urlpatterns = [
    path('', include('django_comments.urls')),
    path('reply/<int:parent>', views.ReplyView.as_view(), name='post_comments_reply'),

现在只要在list.html中的节点加入指向{% url 'comments:post_comments_reply' node.pk %}的链接就可以对节点进行回复。但是这样的方式必须转到一个单独页面进行评论。一般来说我们更希望在本页面进行评论,直接在对应的评论下方直接显示评论框。

我这里是使用iframe标签将表单页面插入进来,但我觉得这并不是一个好的实现方式,但无奈对前端知识不够熟悉,目前就先这样了,以后再改进。

评论中加入一栏iframe内容为url'post_comments_reply'即以评论为父评论的表单页面,设置为隐藏。每个评论用一个btn控制,点击即显示并关闭其他评论的回复。

后面的计划

评论功能到这里就差不多了。第三方账号登录功能这段时间也已经完成了,使用的是all-auth库,加入了github、微博、live、百度账户登录功能。此外还加入了一些像日志记录、错误邮件提醒、国际化、这样零零碎碎的东西,调整了些页面样式。

此外restful也写了一部分,之后逐渐把网站转向完全使用drf的形式。


发表评论


暂无评论

Top