Compare commits

...

17 Commits

@ -0,0 +1,52 @@
#zr 初始数据库迁移文件:创建评论表结构
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 初始迁移
initial = True
#zr 依赖关系
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#zr 迁移操作
operations = [
#zr 创建评论表
migrations.CreateModel(
name='Comment',
fields=[
#zr 主键ID字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#zr 评论正文字段
('body', models.TextField(max_length=300, verbose_name='正文')),
#zr 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#zr 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#zr 是否显示字段
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#zr 文章外键关联
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
#zr 作者外键关联
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#zr 父评论自关联
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
#zr 模型元选项
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,23 @@
#zr 数据库迁移文件:修改评论是否显示字段的默认值
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
('comments', '0001_initial'),
]
#zr 迁移操作
operations = [
#zr 修改comment模型的is_enable字段
migrations.AlterField(
model_name='comment',
name='is_enable',
#zr 将默认值改为False并更新显示名称
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -0,0 +1,73 @@
#zr 数据库迁移文件:更新评论模型字段和选项
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
#zr 迁移操作列表
operations = [
#zr 更新评论模型的元选项
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
#zr 移除旧的创建时间字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
#zr 移除旧的最后修改时间字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
#zr 添加新的创建时间字段
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#zr 添加新的最后修改时间字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#zr 更新文章外键字段配置
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
#zr 更新作者外键字段配置
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
#zr 更新是否启用字段配置
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
#zr 更新父评论外键字段配置
migrations.AlterField(
model_name='comment',
name='parent_comment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
),
]

@ -0,0 +1,56 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
#zr 禁用评论状态的管理动作
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
#zr 启用评论状态的管理动作
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
#zr 设置动作的描述信息
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
#zr 评论管理后台配置类
class CommentAdmin(admin.ModelAdmin):
#zr 设置每页显示数量
list_per_page = 20
#zr 设置列表页显示的字段
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
#zr 设置可点击链接的字段
list_display_links = ('id', 'body', 'is_enable')
#zr 设置过滤器字段
list_filter = ('is_enable',)
#zr 设置排除的表单字段
exclude = ('creation_time', 'last_modify_time')
#zr 设置可用的批量动作
actions = [disable_commentstatus, enable_commentstatus]
#zr 生成用户信息链接的方法
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
#zr 生成文章链接的方法
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
#zr 设置自定义字段的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -0,0 +1,6 @@
from django.apps import AppConfig
#zr 评论应用配置类
class CommentsConfig(AppConfig):
#zr 定义应用名称
name = 'comments'

@ -0,0 +1,37 @@
from django import template
#zr 注册模板标签库
register = template.Library()
#zr 解析评论树的模板标签
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
datas = []
#zr 递归解析子评论的内部函数
def parse(c):
#zr 获取当前评论的直接子评论
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
#zr 将子评论添加到结果列表
datas.append(child)
#zr 递归解析子评论的子评论
parse(child)
#zr 从传入的评论开始解析
parse(comment)
return datas
#zr 显示评论项的包含标签
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
#zr 根据是否为子评论设置不同的深度
depth = 1 if ischild else 2
return {
'comment_item': comment,
'depth': depth
}

@ -0,0 +1,16 @@
from django import forms
from django.forms import ModelForm
from .models import Comment
#zr 评论表单类
class CommentForm(ModelForm):
#zr 父评论ID字段隐藏输入且非必需
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
#zr 指定关联的模型
model = Comment
#zr 指定表单包含的字段
fields = ['body']

@ -0,0 +1,48 @@
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
#zr 评论数据模型
class Comment(models.Model):
#zr 评论正文最大长度300字符
body = models.TextField('正文', max_length=300)
#zr 评论创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#zr 评论最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#zr 评论作者,关联用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
#zr 关联的文章
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
#zr 父级评论,支持评论回复功能
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
#zr 评论是否启用显示
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
#zr 按ID降序排列
ordering = ['-id']
#zr 设置单数和复数显示名称
verbose_name = _('comment')
verbose_name_plural = verbose_name
#zr 指定最新记录的依据字段
get_latest_by = 'id'
def __str__(self):
#zr 返回评论正文作为字符串表示
return self.body

@ -0,0 +1,131 @@
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
from accounts.models import BlogUser
from blog.models import Category, Article
from comments.models import Comment
from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid
# zr 评论模块测试类
class CommentsTest(TransactionTestCase):
# zr 测试初始化设置
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
# zr 导入并设置博客配置,开启评论审核
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.save()
# zr 创建测试用的超级用户
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# zr 更新文章评论状态为启用
def update_article_comment_status(self, article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
# zr 测试评论验证功能
def test_validate_comment(self):
# zr 用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# zr 创建测试分类
category = Category()
category.name = "categoryccc"
category.save()
# zr 创建测试文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
# zr 获取评论提交URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# zr 测试提交第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
self.assertEqual(response.status_code, 302)
# zr 验证评论初始状态为未显示(需要审核)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
# zr 更新评论状态后验证评论显示
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
# zr 测试提交第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
# zr 验证第二条评论
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
# zr 获取父评论ID用于回复测试
parent_comment_id = article.comment_list()[0].id
# zr 测试回复评论包含Markdown格式内容
response = self.client.post(comment_url,
{
'body': '''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
})
self.assertEqual(response.status_code, 302)
# zr 验证回复评论成功
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
# zr 测试评论树解析功能
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1)
# zr 测试评论项显示功能
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
# zr 测试获取最大文章ID和评论ID
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
# zr 测试发送评论邮件功能
from comments.utils import send_comment_email
send_comment_email(comment)

@ -0,0 +1,14 @@
from django.urls import path
from . import views
#zr 定义评论应用的命名空间
app_name = "comments"
#zr 评论模块URL路由配置
urlpatterns = [
#zr 文章评论提交路由
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]

@ -0,0 +1,50 @@
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
# zr 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# zr 发送评论邮件功能
def send_comment_email(comment):
# zr 获取当前站点域名
site = get_current_site().domain
# zr 设置邮件主题
subject = _('Thanks for your comment')
# zr 构建文章完整URL
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# zr 构建给评论作者的邮件内容
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
# zr 获取评论作者邮箱并发送邮件
tomail = comment.author.email
send_email([tomail], subject, html_content)
# zr 如果是回复评论,同时发送邮件给被回复的评论作者
try:
if comment.parent_comment:
# zr 构建回复通知邮件内容
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
# zr 获取被回复评论作者的邮箱并发送通知
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
# zr 记录邮件发送异常
logger.error(e)

@ -0,0 +1,81 @@
# zr 评论视图模块
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
# zr 评论提交视图类
class CommentPostView(FormView):
# zr 使用评论表单类
form_class = CommentForm
# zr 指定模板名称
template_name = 'blog/article_detail.html'
# zr 添加CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
# zr 处理GET请求重定向到文章详情页
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
# zr 处理表单验证失败的情况
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
return self.render_to_response({
'form': form,
'article': article
})
# zr 处理表单验证成功的情况
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# zr 获取当前用户信息
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
# zr 获取文章信息
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
# zr 检查文章是否允许评论
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# zr 创建评论对象但不立即保存到数据库
comment = form.save(False)
comment.article = article
# zr 获取博客设置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
# zr 处理回复评论的情况
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
# zr 保存评论到数据库
comment.save(True)
# zr 重定向到文章页面并定位到新评论
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

Binary file not shown.
Loading…
Cancel
Save