原本自己写的评论模块功能很简单,不能实现现在很常见的多层评论功能。于是决定把它重构一下。 使用的是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
用于关联父评。
重写初始化方法,向表单传入user
、 parent
两个属性。
与模型类似也要在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_for
和render_list_for
分别对应的是form.html
和list.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的形式。
暂无评论