comments模块代码标注

pull/4/head
商世浚 4 months ago
parent 019fc5d0c5
commit 7f91075871

@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="jdk" jdkName="Python 3.9 (BookManage)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">

@ -2,11 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<<<<<<< HEAD
<module fileurl="file://$PROJECT_DIR$/.idea/djangoBlogStudy.iml" filepath="$PROJECT_DIR$/.idea/djangoBlogStudy.iml" />
=======
<module fileurl="file://$PROJECT_DIR$/.idea/djangoBlog.iml" filepath="$PROJECT_DIR$/.idea/djangoBlog.iml" />
>>>>>>> master
</modules>
</component>
</project>

@ -1,47 +1,106 @@
# 导入 Django 管理后台模块
from django.contrib import admin
# 导入 reverse 函数,用于反向解析 URL根据命名 URL 生成实际路径)
from django.urls import reverse
# 导入 format_html用于安全地格式化 HTML 字符串(防止 XSS
from django.utils.html import format_html
# 导入 gettext_lazy 作为 _用于字符串国际化延迟翻译
from django.utils.translation import gettext_lazy as _
# 自定义管理员操作:批量禁用选中的评论
def disable_commentstatus(modeladmin, request, queryset):
"""
将选中的评论设置为不启用状态即不显示
此函数将在评论管理界面作为批量操作使用
"""
# 批量更新:将 queryset 中所有评论的 is_enable 字段设为 False
queryset.update(is_enable=False)
# 自定义管理员操作:批量启用选中的评论
def enable_commentstatus(modeladmin, request, queryset):
"""
将选中的评论设置为启用状态即显示
此函数将在评论管理界面作为批量操作使用
"""
# 批量更新:将 queryset 中所有评论的 is_enable 字段设为 True
queryset.update(is_enable=True)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
# 为自定义操作设置在管理界面中显示的描述文本(支持国际化)
disable_commentstatus.short_description = _('Disable comments') # 显示为“禁用评论”
enable_commentstatus.short_description = _('Enable comments') # 显示为“启用评论”
# 定义 Comment 模型在 Django 管理后台的显示和操作配置
class CommentAdmin(admin.ModelAdmin):
# 设置评论列表每页显示 20 条数据
list_per_page = 20
# 定义在评论列表页面显示的字段列
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
'id', # 评论的数据库 ID
'body', # 评论正文内容
'link_to_userinfo', # 自定义方法:显示作者信息(带链接)
'link_to_article', # 自定义方法:显示所属文章(带链接)
'is_enable', # 是否启用(布尔值,显示为开关)
'creation_time' # 评论创建时间
)
# 设置哪些字段可以点击,跳转到该评论的编辑页面
list_display_links = ('id', 'body', 'is_enable')
# 添加右侧过滤侧边栏,允许按 is_enable 字段(是否启用)筛选评论
list_filter = ('is_enable',)
# 在添加或编辑评论时,从表单中排除这两个字段
# 因为 creation_time 和 last_modify_time 通常由代码自动处理(如默认值或保存时更新)
exclude = ('creation_time', 'last_modify_time')
# 注册自定义的批量操作,允许管理员在列表页选择多条评论执行“启用”或“禁用”
actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj):
"""
自定义列表字段生成指向评论作者用户信息编辑页面的超链接
参数
obj: 当前评论对象
返回
HTML 格式的链接链接文本为用户的昵称若有否则为邮箱
"""
# 获取作者用户模型的 app_label 和 model_name如 'auth', 'user'
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 使用 reverse 构造 Django 管理后台中该用户的编辑页面 URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 返回一个安全的 HTML 链接
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
def link_to_article(self, obj):
"""
自定义列表字段生成指向评论所属文章编辑页面的超链接
参数
obj: 当前评论对象
返回
HTML 格式的链接链接文本为文章标题
"""
# 获取文章模型的 app_label 和 model_name如 'blog', 'article'
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 构造该文章在管理后台的编辑页面 URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 返回一个安全的 HTML 链接
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
# 为自定义的列表字段设置列标题(支持国际化)
link_to_userinfo.short_description = _('User') # 列标题显示为“用户”
link_to_article.short_description = _('Article') # 列标题显示为“文章”

@ -1,5 +1,9 @@
# 导入 Django 应用配置基类
from django.apps import AppConfig
# 定义 comments 应用的配置类
class CommentsConfig(AppConfig):
# 指定该配置对应的 Django 应用的完整 Python 路径
# 即当前应用的包名(位于 INSTALLED_APPS 中)
name = 'comments'

@ -4,10 +4,16 @@ from django.forms import ModelForm
from .models import Comment
# 定义一个用于处理评论数据的表单类,继承自 Django 的 ModelForm
class CommentForm(ModelForm):
# 自定义字段parent_comment_id
# 用于存储当前评论所回复的父评论的 ID
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
widget=forms.HiddenInput, # 使用隐藏输入框HTML <input type="hidden">),不在页面上显示
required=False # 非必填字段,因为一级评论没有父评论
)
class Meta:
model = Comment
fields = ['body']
model = Comment # 关联的数据库模型为 Comment
fields = ['body'] # 表单中需要包含的模型字段,仅包含 'body'(评论正文)
# 注意:其他字段如 author、article、creation_time 等通常在视图中自动填充,不暴露给用户

@ -1,5 +1,3 @@
# 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
@ -7,32 +5,69 @@ import django.utils.timezone
class Migration(migrations.Migration):
# 表示这是一个初始迁移(即创建模型的第一次迁移)
initial = True
# 定义该迁移所依赖的其他迁移
# 只有当这些依赖的迁移执行完成后,当前迁移才会执行
dependencies = [
# 依赖于 blog 应用下的 '0001_initial' 迁移
# 确保 blog 应用中的模型(如 Article已创建
('blog', '0001_initial'),
# 依赖于用户模型的迁移
# 使用 migrations.swappable_dependency 和 settings.AUTH_USER_MODEL
# 是为了支持自定义用户模型(即项目可能使用了非默认的 User 模型)
# Django 会自动解析为实际使用的用户模型对应的迁移
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 定义本次迁移要执行的数据库操作列表
operations = [
# 创建一个名为 'Comment' 的数据库模型(对应一张数据表)
migrations.CreateModel(
name='Comment',
fields=[
name='Comment', # 模型名称
fields=[ # 模型包含的字段列表
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 主键字段id
# 使用 BigAutoField64位整数自动递增作为主键不序列化为单独字段serialize=False
('body', models.TextField(max_length=300, verbose_name='正文')),
# 正文字段body
# 文本字段最大长度300字符显示名称为“正文”
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 创建时间字段created_time
# DateTimeField默认值为当前时间使用 django.utils.timezone.now 函数)
# 显示名称为“创建时间”
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 最后修改时间字段last_mod_time
# DateTimeField默认值也为当前时间初始创建时与创建时间相同
# 后续可通过逻辑更新
# 显示名称为“修改时间”
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 是否启用字段is_enable
# BooleanField默认为 True表示评论是否显示
# 用于软删除或审核功能
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
# 外键字段article
# 关联到 blog 应用中的 Article 模型('blog.article'
# on_delete=models.CASCADE当文章被删除时该评论也会被级联删除
# 显示名称为“文章”
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 外键字段author
# 关联到当前项目配置的用户模型(支持自定义用户)
# 当用户被删除时,该用户的评论也会被级联删除
# 显示名称为“作者”
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
# 外键字段parent_comment
# 实现评论的嵌套/回复功能指向同一模型comments.Comment的其他评论
# blank=True, null=True允许为空表示可以是一级评论顶级评论
# on_delete=CASCADE上级评论被删除时其子评论也一并删除
# 显示名称为“上级评论”
],
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
options={ # 模型的元选项Meta options
'verbose_name': '评论', # 单数形式的可读名称
'verbose_name_plural': '评论', # 复数形式的可读名称(此处未复数化,与单数相同)
'ordering': ['-id'], # 默认排序:按 id 降序排列(最新的在前)
'get_latest_by': 'id', # 使用 latest() 方法时,默认按 id 获取最新一条记录
},
),
]

@ -1,18 +1,25 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
# migrations: 用于定义数据库迁移操作
# models: 用于定义模型字段类型
class Migration(migrations.Migration):
# 定义一个迁移类,继承自 django.db.migrations.Migration
dependencies = [
('comments', '0001_initial'),
]
# 当前迁移的依赖列表
# 表示必须先执行 comments 应用下的 '0001_initial' 迁移(即初始创建 Comment 模型的迁移)
# 之后才能执行当前迁移
operations = [ # 定义本次迁移要执行的数据库操作列表
migrations.AlterField( # 执行一个“修改字段”的操作
model_name='comment', # 要修改的模型名称(对应 comments 应用中的 Comment 模型)
name='is_enable', # 要修改的字段名
field=models.BooleanField( # 新的字段定义
default=False, # 将默认值从之前的 True 修改为 False
verbose_name='是否显示'), # 字段的可读名称保持不变
operations = [
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -3,55 +3,65 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import django.utils.timezone # 用于引用 AUTH_USER_MODEL
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
dependencies = [ # 当前迁移所依赖的其他迁移,必须按顺序执行完这些依赖迁移后,才能执行本迁移
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖于 blog 应用的第 5 个迁移(可能修改了 Article 或 Category 的选项)
('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖于 blog 应用的第 5 个迁移(可能修改了 Article 或 Category 的选项)
('comments', '0002_alter_comment_is_enable'), # 依赖于 comments 应用的第 2 个迁移(之前已修改过 is_enable 字段的默认值)
]
operations = [
operations = [ # 定义本次迁移要执行的所有数据库操作
# 1. 修改模型的元选项Meta options
migrations.AlterModelOptions(
name='comment',
name='comment', # 作用于 Comment 模型
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
# 单数名称改为英文小写 # 复数名称也改为英文 # 排序方式不变:按 id 降序(最新在前) # 获取最新记录仍按 id
),
# 2. 删除旧的创建时间字段created_time
migrations.RemoveField(
model_name='comment',
name='created_time',
),
# 3. 删除旧的最后修改时间字段last_mod_time
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
# 4. 添加新的创建时间字段creation_time替代 created_time
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 5. 添加新的最后修改时间字段last_modify_time替代 last_mod_time
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
# 6. 修改 article 外键字段:仅更新 verbose_name 为英文
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
# 7. 修改 author 外键字段:仅更新 verbose_name 为英文
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'),
),
# 8. 修改 is_enable 字段:更新 verbose_name 为英文,并保持 default=False
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
# 9. 修改 parent_comment 字段:更新 verbose_name 为英文
migrations.AlterField(
model_name='comment',
name='parent_comment',

@ -6,34 +6,78 @@ from django.utils.translation import gettext_lazy as _
from blog.models import Article
# Create your models here.
# 定义评论模型,用于存储用户对文章的评论数据
class Comment(models.Model):
# 评论正文内容
# 使用 TextField 存储较长文本,限制最大长度为 300 字符
# verbose_name 设置为 '正文',在管理后台等界面中显示为字段标签
body = models.TextField('正文', max_length=300)
# 评论创建时间
# 自动记录评论的创建时间默认值为当前时间now
# verbose_name 为国际化字符串 'creation time'
creation_time = models.DateTimeField(_('creation time'), default=now)
# 评论最后修改时间
# 记录评论最后一次被修改的时间,默认值为当前时间(通常在保存时更新)
# verbose_name 为国际化字符串 'last modify time'
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 评论作者
# 外键关联到用户模型(支持自定义用户模型)
# verbose_name 为 'author'国际化on_delete=models.CASCADE 表示用户删除时,其评论也级联删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
# 所属文章
# 外键关联到 Article 模型,表示该评论属于哪篇文章
# verbose_name 为 'article'(国际化),级联删除:文章删除时,其所有评论也被删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
# 父级评论(用于实现嵌套评论/回复功能)
# 外键指向自身('self'),实现树形结构
# blank=True, null=True 表示可以为空(即一级评论没有父评论)
# verbose_name 为 'parent comment'(国际化),级联删除:父评论删除时,其子评论也删除
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
# 是否启用(是否显示)
# BooleanField 用于控制评论是否公开显示
# default=False 表示默认不启用(需审核或手动开启)
# blank=False, null=False 表示该字段不可为空,必须有值
# verbose_name 为 'enable'(国际化)
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
"""
模型元数据类定义模型的元信息如排序名称等
"""
# 默认排序:按 id 降序排列(最新的评论在前)
ordering = ['-id']
# 模型的可读名称(单数形式),用于管理后台等界面显示
verbose_name = _('comment')
# 模型的可读名称(复数形式),此处与单数相同
verbose_name_plural = verbose_name
# 指定获取最新一条记录时依据的字段
get_latest_by = 'id'
def __str__(self):
"""
返回该评论对象的字符串表示
通常在管理后台或调试时显示
此处返回评论的正文内容body
"""
return self.body

@ -1,5 +1,7 @@
# 导入 Django 模板系统模块
from django import template
# 创建一个模板标签库实例,用于注册自定义模板标签和过滤器
register = template.Library()
@ -8,23 +10,31 @@ def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
# 存储所有子评论的列表
datas = []
def parse(c):
# 从 commentlist 中筛选出 parent_comment 指向当前评论 c 且 is_enable=True 的子评论
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
# 将符合条件的子评论添加到结果列表中
datas.append(child)
# 递归查找该子评论的子评论,实现深度优先遍历
parse(child)
# 从传入的根评论开始递归解析
parse(comment)
# 返回最终收集到的所有子评论列表
return datas
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
# 根据是否为子评论设置缩进层级
# 如果是子评论ischild=Truedepth=1否则 depth=2
depth = 1 if ischild else 2
return {
'comment_item': comment,
'depth': depth
'comment_item': comment, # 当前评论对象
'depth': depth # 缩进层级,用于模板中控制显示样式
}

@ -10,71 +10,99 @@ from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
# 定义一个基于事务的测试用例类,用于测试 comments 应用的相关功能
class CommentsTest(TransactionTestCase):
def setUp(self):
# 创建 Django 测试客户端,用于模拟用户请求
self.client = Client()
# 创建请求工厂,用于在视图测试中构造请求对象
self.factory = RequestFactory()
# 导入博客设置模型(由于导入在方法内,可能为了延迟加载或避免循环导入)
from blog.models import BlogSettings
# 创建一个博客全局设置实例
value = BlogSettings()
# 设置评论需要审核(即新评论默认不显示,需管理员启用)
value.comment_need_review = True
# 保存到数据库
value.save()
# 创建一个超级用户,用于登录和发表评论
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
def update_article_comment_status(self, article):
# 获取该文章的所有评论
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
comment.is_enable = True # 启用评论
comment.save() # 保存更改
def test_validate_comment(self):
# 使用测试用户登录(确保有权限发表评论)
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 创建文章分类
category = Category()
category.name = "categoryccc"
category.save()
# 创建一篇文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' # 普通文章类型
article.status = 'p' # 已发布状态
article.save()
# 反向解析获取评论提交的 URL根据命名空间和参数
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# 模拟 POST 请求发表第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
# 断言:响应状态码应为 302重定向表示评论提交成功
self.assertEqual(response.status_code, 302)
# 重新获取文章对象(刷新数据)
article = Article.objects.get(pk=article.pk)
# 此时评论未审核comment_list() 返回的可见评论数应为 0
self.assertEqual(len(article.comment_list()), 0)
self.update_article_comment_status(article)
# 手动启用该评论(模拟审核通过)
self.update_article_comment_status(article)
# 此时可见评论数应为 1
self.assertEqual(len(article.comment_list()), 1)
# 再次发表第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
# 断言:响应成功(重定向)
self.assertEqual(response.status_code, 302)
# 刷新文章数据并启用新评论
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
# 总共应有 2 条可见评论
self.assertEqual(len(article.comment_list()), 2)
# 获取第一条评论的 ID用于后续回复作为父评论
parent_comment_id = article.comment_list()[0].id
# 发表一条回复(嵌套评论),包含复杂 Markdown 内容
response = self.client.post(comment_url,
{
'body': '''
@ -88,22 +116,37 @@ class CommentsTest(TransactionTestCase):
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
'parent_comment_id': parent_comment_id # 指定父评论
})
# 断言:提交成功
self.assertEqual(response.status_code, 302)
# 启用新评论并刷新文章数据
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
# 总评论数应为 32 条一级 + 1 条子评论)
self.assertEqual(len(article.comment_list()), 3)
# 获取父评论对象
comment = Comment.objects.get(id=parent_comment_id)
# 解析从该父评论开始的所有子评论树
tree = parse_commenttree(article.comment_list(), comment)
# 子评论树中应包含 1 个子评论
self.assertEqual(len(tree), 1)
# 测试包含标签 show_comment_item 是否正常返回数据
data = show_comment_item(comment, True)
# 返回值不应为 None
self.assertIsNotNone(data)
# 调用工具函数获取最大文章ID和评论ID可能是用于生成唯一标识
s = get_max_articleid_commentid()
# 函数应返回有效值
self.assertIsNotNone(s)
# 从 utils 模块导入发送评论邮件函数
from comments.utils import send_comment_email
send_comment_email(comment)
# 测试发送评论通知邮件功能
send_comment_email(comment)

@ -2,10 +2,28 @@ from django.urls import path
from . import views
# 定义当前应用comments的 URL 命名空间
# 在项目其他地方可以通过 'comments:xxx' 的方式反向解析 URL
app_name = "comments"
# 定义 comments 应用的 URL 路由列表
urlpatterns = [
# 路由配置:将特定 URL 模式映射到对应的视图处理逻辑
path(
# URL 模式:
# - 以 'article/' 开头
# - 接一个整数类型的 article_id 参数(通过 <int:article_id> 捕获)
# - 然后是 'postcomment' 路径
# 例如:/comments/article/123/postcomment/
'article/<int:article_id>/postcomment',
# 视图处理类:
# 使用基于类的视图 (Class-Based View) CommentPostView 的 as_view() 方法
# as_view() 将类视图转换为可调用的视图函数,供 URL 路由使用
views.CommentPostView.as_view(),
# URL 名称:
# 为该路由设置一个唯一名称 'postcomment'
# 在模板或代码中可通过 {% url 'comments:postcomment' article_id=123 %} 的方式引用
name='postcomment'),
]
]

@ -5,24 +5,44 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
# 获取当前模块的 logger 实例,用于记录日志
logger = logging.getLogger(__name__)
def send_comment_email(comment):
# 获取当前站点的域名(如 example.com
site = get_current_site().domain
# 邮件主题(支持国际化)
subject = _('Thanks for your comment')
# 构造文章的完整绝对 URL使用 HTTPS
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 构造发送给评论作者的 HTML 邮件内容
# 内容包含感谢语、文章链接和标题,并支持变量替换
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}
%(article_url)s""") % {
'article_url': article_url,
'article_title': comment.article.title
}
# 收件人邮箱:当前发表评论的用户
tomail = comment.author.email
# 调用发送邮件的通用函数,发送感谢邮件
send_email([tomail], subject, html_content)
try:
# 如果当前评论是回复(即存在父评论)
if comment.parent_comment:
# 构造发送给被回复用户的 HTML 邮件内容
# 通知其评论收到了新的回复
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/>
@ -30,9 +50,18 @@ def send_comment_email(comment):
<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}
""") % {
'article_url': article_url,
'article_title': comment.article.title,
'comment_body': comment.parent_comment.body # 被回复的原始评论内容
}
# 收件人邮箱:被回复的评论的作者
tomail = comment.parent_comment.author.email
# 发送回复通知邮件
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)
# 如果在发送通知邮件过程中发生异常(如用户无邮箱等),记录错误日志
logger.error(e)

@ -12,8 +12,12 @@ from .forms import CommentForm
from .models import Comment
# 定义一个基于类的视图,用于处理用户提交评论的请求
class CommentPostView(FormView):
# 指定该视图使用的表单类
form_class = CommentForm
# 指定表单验证失败时,或需要渲染响应时使用的模板
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect)
@ -21,15 +25,26 @@ class CommentPostView(FormView):
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
# 从 URL 参数中获取文章 ID
article_id = self.kwargs['article_id']
# 根据 ID 获取文章对象,若不存在则返回 404 错误
article = get_object_or_404(Article, pk=article_id)
# 获取文章的绝对 URL
url = article.get_absolute_url()
# 重定向到文章页面,并锚定到 id="comments" 的元素(通常是评论列表)
return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form):
# 获取文章 ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象404 安全获取)
article = get_object_or_404(Article, pk=article_id)
# 渲染模板,将表单(含错误)和文章对象传递给模板
return self.render_to_response({
'form': form,
'article': article
@ -37,27 +52,57 @@ class CommentPostView(FormView):
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# 获取当前登录的用户
user = self.request.user
# 获取用户对应的 BlogUser 对象
author = BlogUser.objects.get(pk=user.pk)
# 获取 URL 中的文章 ID
article_id = self.kwargs['article_id']
# 获取对应的文章对象404 安全获取)
article = get_object_or_404(Article, pk=article_id)
# 检查文章是否允许评论
# 如果文章的评论状态为 'c'(关闭)或文章状态为 'c'(草稿等),则禁止评论
if article.comment_status == 'c' or article.status == 'c':
# 抛出验证错误,阻止评论提交
raise ValidationError("该文章评论已关闭.")
# 调用表单的 save 方法但暂不提交到数据库commit=False
comment = form.save(False)
# 设置评论所属的文章
comment.article = article
# 导入博客设置工具函数
from djangoblog.utils import get_blog_setting
# 获取当前博客的全局设置
settings = get_blog_setting()
# 如果全局设置中不需要审核评论,则直接启用该评论
if not settings.comment_need_review:
comment.is_enable = True
# 设置评论作者
comment.author = author
# 检查表单中是否包含父评论 ID即是否为回复
if form.cleaned_data['parent_comment_id']:
# 根据父评论 ID 获取父评论对象
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
# 建立父子评论关系
comment.parent_comment = parent_comment
# 将评论对象保存到数据库(此时所有字段已设置完毕)
comment.save(True)
# 重定向到文章详情页,并定位到刚刚发表的评论
# 使用锚点 #div-comment-{id} 定位到具体评论
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
(article.get_absolute_url(), comment.pk))
Loading…
Cancel
Save