更新代码 #9

Open
pzfagqo67 wants to merge 0 commits from gst_branch into develop

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/djangoblog/src/DjangoBlog-master/DjangoBlog-master" />
<option name="settingsModule" value="settings.py" />
<option name="manageScript" value="$MODULE_DIR$/djangoblog/src/DjangoBlog-master/DjangoBlog-master/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/djangoblog/src/DjangoBlog-master/DjangoBlog-master" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (DjangoBlog)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
<option name="sdkName" value="Python 3.12 (DjangoBlog)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (DjangoBlog)" project-jdk-type="Python SDK" />
</project>

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/zyl_django.iml" filepath="$PROJECT_DIR$/.idea/zyl_django.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/DjangoBlog-gst_branch.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog-gst_branch.iml" />
</modules>
</component>
</project>

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/zyl_django.iml" filepath="$PROJECT_DIR$/.idea/zyl_django.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,84 @@
# gst: 导入Django admin模块用于自定义后台管理功能
from django.contrib import admin
# gst: 导入reverse用于生成后台管理页面的URL
from django.urls import reverse
# gst: 导入format_html用于在管理界面生成HTML链接
from django.utils.html import format_html
# gst: 导入gettext_lazy用于国际化翻译别名_
from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset):
"""
gst: 定义admin动作函数批量将选中的评论设置为不显示
:param modeladmin: 模型管理类实例
:param request: 请求对象
:param queryset: 选中的评论查询集
"""
queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset):
"""
gst: 定义admin动作函数批量将选中的评论设置为显示
:param modeladmin: 模型管理类实例
:param request: 请求对象
:param queryset: 选中的评论查询集
"""
queryset.update(is_enable=True)
# gst: 为动作函数设置后台显示名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
"""
gst: 自定义Comment模型的后台管理类配置列表显示过滤动作等功能
"""
list_per_page = 20 # gst: 后台列表每页显示20条数据
list_display = (
'id',
'body',
'link_to_userinfo', # gst: 自定义列,显示评论作者的可点击链接
'link_to_article', # gst: 自定义列,显示评论关联文章的可点击链接
'is_enable',
'creation_time'
)
list_display_links = ('id', 'body', 'is_enable') # gst: 这些字段可点击进入详情页
list_filter = ('is_enable',) # gst: 后台列表过滤条件(按是否显示过滤)
exclude = ('creation_time', 'last_modify_time') # gst: 编辑页排除这些字段(不允许编辑)
actions = [disable_commentstatus, enable_commentstatus] # gst: 注册批量动作
def link_to_userinfo(self, obj):
"""
gst: 生成评论作者的后台管理链接点击可进入用户详情页
:param obj: Comment模型实例
:return: 带HTML链接的作者信息
"""
# 获取用户模型的app_label和model_name用于生成URL
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 生成用户详情页的URL如admin/auth/user/1/change/
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 优先显示用户昵称,无昵称则显示邮箱
display_text = obj.author.nickname if obj.author.nickname else obj.author.email
# 生成带链接的HTML内容
return format_html(u'<a href="%s">%s</a>' % (link, display_text))
def link_to_article(self, obj):
"""
gst: 生成评论关联文章的后台管理链接点击可进入文章详情页
:param obj: Comment模型实例
:return: 带HTML链接的文章标题
"""
# 获取文章模型的app_label和model_name用于生成URL
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 生成文章详情页的URL如admin/blog/article/1/change/
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))
# gst: 为自定义列设置后台显示名称(支持国际化)
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -0,0 +1,11 @@
# gst: 导入Django的AppConfig类用于配置应用的元数据和初始化行为
from django.apps import AppConfig
class CommentsConfig(AppConfig):
"""
gst: 评论应用comments的配置类继承自Django的AppConfig
用于定义应用的核心配置信息Django启动时会自动加载该配置
"""
# gst: 应用的唯一标识名称必须与应用目录名一致用于Django识别和引用该应用
name = 'comments'

@ -0,0 +1,23 @@
# gst: 导入Django表单核心模块用于构建表单基础功能
from django import forms
# gst: 导入ModelForm模型表单可直接关联数据模型快速生成表单
from django.forms import ModelForm
# gst: 导入当前应用的Comment模型表单将与该模型关联
from .models import Comment
class CommentForm(ModelForm):
"""
gst: 评论表单类继承自ModelForm用于处理评论提交含回复功能
自动关联Comment模型字段简化表单验证和数据保存逻辑
"""
# gst: 父评论ID字段用于实现评论回复功能
# 隐藏输入HiddenInput用户不可见仅用于传递上级评论ID
# required=False 表示非必填(普通评论无父评论时可不传)
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
model = Comment # gst: 关联的数据库模型Comment
fields = ['body'] # gst: 表单需要渲染的模型字段仅评论正文body

@ -0,0 +1,37 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings # gst: 导入Django项目配置包含用户模型等核心配置
from django.db import migrations, models # gst: 导入数据库迁移与模型字段相关模块
import django.db.models.deletion # gst: 导入外键删除行为处理模块
import django.utils.timezone # gst: 导入Django时区工具用于时间字段默认值
class Migration(migrations.Migration): # gst: 定义迁移类,管理数据库模型的迁移操作
initial = True # gst: 标记为初始迁移(首次创建该模型的迁移)
dependencies = [ # gst: 迁移依赖项,指定执行当前迁移前需完成的其他迁移
('blog', '0001_initial'), # gst: 依赖blog应用的0001号初始迁移确保Article模型已存在
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # gst: 依赖可替换的用户模型(支持自定义用户扩展)
]
operations = [ # gst: 迁移操作列表,定义当前迁移要执行的数据库操作
migrations.CreateModel( # gst: 执行“创建模型”的迁移操作
name='Comment', # gst: 模型名称对应数据库表comments_comment
fields=[ # gst: 模型字段定义集合
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # gst: 自增主键字段,适用于大数据量场景
('body', models.TextField(max_length=300, verbose_name='正文')), # gst: 评论正文字段文本类型且限制最大长度300后台显示“正文”
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # gst: 创建时间字段,默认值为当前时区时间,后台显示“创建时间”
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # gst: 修改时间字段,默认值为当前时区时间,后台显示“修改时间”
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # gst: 布尔字段,控制评论是否显示(默认显示),后台显示“是否显示”
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), # gst: 外键关联blog应用的Article模型删除文章时级联删除评论后台显示“文章”
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # gst: 外键关联用户模型,删除用户时级联删除评论,后台显示“作者”
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), # gst: 自关联外键(支持评论回复),允许为空,删除上级评论时级联删除子评论,后台显示“上级评论”
],
options={ # gst: 模型额外配置项
'verbose_name': '评论', # gst: 后台管理中模型的单数显示名称
'verbose_name_plural': '评论', # gst: 后台管理中模型的复数显示名称
'ordering': ['-id'], # gst: 数据查询默认排序(按主键倒序,最新评论在前)
'get_latest_by': 'id', # gst: 调用latest()方法时按id字段获取最新记录
},
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models # gst: 导入数据库迁移和模型字段相关模块
class Migration(migrations.Migration): # gst: 定义迁移类,管理数据库模型的修改操作
dependencies = [ # gst: 迁移依赖项,指定执行当前迁移前需完成的其他迁移
('comments', '0001_initial'), # gst: 依赖comments应用的0001号初始迁移确保Comment模型已存在
]
operations = [ # gst: 迁移操作列表,定义当前迁移要执行的数据库操作
migrations.AlterField( # gst: 执行“修改字段”的迁移操作
model_name='comment', # gst: 要修改的模型名称Comment
name='is_enable', # gst: 要修改的字段名称(是否显示字段)
field=models.BooleanField(default=False, verbose_name='是否显示'), # gst: 修改后字段类型为布尔型默认值改为False后台显示名称为“是否显示”
),
]

@ -0,0 +1,64 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings # gst: 导入Django项目配置含用户模型配置
from django.db import migrations, models # gst: 导入数据库迁移和模型字段相关模块
import django.db.models.deletion # gst: 导入外键删除行为相关模块
import django.utils.timezone # gst: 导入Django时区工具用于时间字段默认值
class Migration(migrations.Migration): # gst: 定义迁移类,管理数据库模型的修改操作
dependencies = [ # gst: 迁移依赖项,指定执行当前迁移前需完成的其他迁移
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # gst: 依赖可替换的用户模型(支持自定义用户)
('blog', '0005_alter_article_options_alter_category_options_and_more'), # gst: 依赖blog应用的0005号迁移确保Article模型结构最新
('comments', '0002_alter_comment_is_enable'), # gst: 依赖comments应用的0002号迁移确保Comment模型已有基础修改
]
operations = [ # gst: 迁移操作列表,定义当前迁移要执行的数据库操作
migrations.AlterModelOptions( # gst: 执行“修改模型选项”的迁移操作
model_name='comment', # gst: 要修改的模型名称Comment
options={ # gst: 模型选项的新配置
'get_latest_by': 'id', # gst: 使用latest()方法时按id字段获取最新记录
'ordering': ['-id'], # gst: 数据查询默认排序:按主键倒序(最新评论在前)
'verbose_name': 'comment', # gst: 后台管理中模型的单数显示名称(改为英文)
'verbose_name_plural': 'comment', # gst: 后台管理中模型的复数显示名称(改为英文)
},
),
migrations.RemoveField( # gst: 执行“删除字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='created_time', # gst: 要删除的字段名称(原创建时间字段)
),
migrations.RemoveField( # gst: 执行“删除字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='last_mod_time', # gst: 要删除的字段名称(原修改时间字段)
),
migrations.AddField( # gst: 执行“添加字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='creation_time', # gst: 新增字段名称创建时间替换原created_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # gst: 字段类型为日期时间默认值为当前时区时间后台显示名称为“creation time”
),
migrations.AddField( # gst: 执行“添加字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='last_modify_time', # gst: 新增字段名称修改时间替换原last_mod_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # gst: 字段类型为日期时间默认值为当前时区时间后台显示名称为“last modify time”
),
migrations.AlterField( # gst: 执行“修改字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='article', # gst: 要修改的字段名称(关联文章字段)
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), # gst: 外键关联blog应用的Article模型删除文章时级联删除评论后台显示名称为“article”
),
migrations.AlterField( # gst: 执行“修改字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='author', # gst: 要修改的字段名称(关联作者字段)
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), # gst: 外键关联用户模型删除用户时级联删除评论后台显示名称为“author”
),
migrations.AlterField( # gst: 执行“修改字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='is_enable', # gst: 要修改的字段名称(是否显示字段)
field=models.BooleanField(default=False, verbose_name='enable'), # gst: 字段类型为布尔型默认值为False后台显示名称为“enable”
),
migrations.AlterField( # gst: 执行“修改字段”的迁移操作
model_name='comment', # gst: 要操作的模型Comment
name='parent_comment', # gst: 要修改的字段名称(上级评论字段)
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'), # gst: 自关联外键支持评论回复允许为空删除上级评论时级联删除子评论后台显示名称为“parent comment”
),
]

@ -0,0 +1,61 @@
# gst: 导入Django项目配置含用户模型等核心配置
from django.conf import settings
# gst: 导入Django数据库模型相关模块用于定义数据模型
from django.db import models
# gst: 导入Django时区工具用于时间字段默认值
from django.utils.timezone import now
# gst: 导入国际化翻译工具别名_支持多语言显示
from django.utils.translation import gettext_lazy as _
# gst: 导入blog应用的Article模型用于关联评论与文章
from blog.models import Article
# Create your models here.
class Comment(models.Model):
"""
gst: 评论数据模型存储博客文章的评论信息支持评论回复功能
关联用户文章模型包含评论正文时间状态等核心字段
"""
# 评论正文文本字段最大长度300字符后台管理显示名称为“正文”
body = models.TextField('正文', max_length=300)
# 创建时间:日期时间字段,支持国际化翻译,默认值为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间:日期时间字段,支持国际化翻译,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 关联作者外键关联Django用户模型支持国际化翻译
# 删除用户时级联删除该用户的所有评论CASCADE
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
# 关联文章外键关联blog应用的Article模型支持国际化翻译
# 删除文章时级联删除该文章的所有评论CASCADE
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
# 父评论:自关联外键,支持评论回复功能,支持国际化翻译
# blank=True 允许表单提交为空null=True 允许数据库存储NULL值
# 删除父评论时级联删除所有子评论CASCADE
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
# 是否启用布尔字段支持国际化翻译默认值为False不显示
# blank=False 和 null=False 表示该字段为必填项,不允许为空
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
ordering = ['-id'] # 数据查询默认排序:按主键倒序(最新评论在前)
verbose_name = _('comment') # 后台管理模型单数名称(支持国际化)
verbose_name_plural = verbose_name # 后台管理模型复数名称(与单数一致)
get_latest_by = 'id' # 使用latest()方法时按id字段获取最新记录
def __str__(self):
"""gst: 模型实例的字符串表示,返回评论正文(方便后台管理查看)"""
return self.body

@ -0,0 +1,46 @@
from django import template
# gst: 注册模板标签库,使自定义标签可在模板中使用
register = template.Library()
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""
gst: 定义simple_tag用于获取当前评论的所有子评论列表递归遍历
用法: {% parse_commenttree article_comments comment as childcomments %}
:param commentlist: 评论查询集需包含所有待筛选的评论
:param comment: 父评论对象以此为根节点查找子评论
:return: 子评论列表包含多级嵌套的子评论
"""
datas = []
def parse(c):
"""
gst: 递归函数用于遍历父评论的所有子评论并加入结果列表
:param c: 当前父评论对象
"""
# 筛选出当前父评论的有效子评论is_enable=True
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
datas.append(child) # 将子评论加入结果列表
parse(child) # 递归遍历该子评论的子评论
parse(comment) # 从传入的comment开始递归解析
return datas
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""
gst: 定义inclusion_tag用于渲染评论项模板区分父子评论的显示层级
:param comment: 要渲染的评论对象
:param ischild: 是否为子评论布尔值
:return: 模板上下文包含评论对象和显示层级
"""
# 根据是否为子评论设置深度父评论深度为2子评论深度为1
depth = 1 if ischild else 2
return {
'comment_item': comment, # 传递评论对象到模板
'depth': depth # 传递显示层级到模板
}

@ -0,0 +1,165 @@
# gst: 导入Django测试相关核心模块用于模拟请求和执行测试
from django.test import Client, RequestFactory, TransactionTestCase
# gst: 导入reverse用于反向解析URL路径
from django.urls import reverse
# gst: 导入关联数据模型,为测试准备基础数据
from accounts.models import BlogUser
from blog.models import Category, Article
from comments.models import Comment
# gst: 导入自定义评论模板标签,测试其功能可用性
from comments.templatetags.comments_tags import *
# gst: 导入工具函数,测试辅助功能
from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
class CommentsTest(TransactionTestCase):
"""
gst: 评论功能集成测试类继承TransactionTestCase以支持事务管理
主要测试评论提交审核回复模板标签及工具函数等核心流程
"""
def setUp(self):
"""
gst: 测试前置初始化方法每个测试用例执行前自动执行
初始化测试客户端博客设置和测试用户
"""
# gst: 初始化Django测试客户端用于模拟用户发起HTTP请求
self.client = Client()
# gst: 初始化RequestFactory用于构造自定义请求对象
self.factory = RequestFactory()
# gst: 导入博客设置模型,配置评论需要审核的规则
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True # gst: 开启评论审核,提交后需审核才显示
value.save()
# gst: 创建超级用户,用于测试登录状态下的评论操作
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
def update_article_comment_status(self, article):
"""
gst: 辅助测试方法用于批量启用指定文章的所有评论
模拟评论审核通过的场景方便后续测试评论显示逻辑
:param article: 接收文章对象对其关联的评论进行状态更新
"""
# gst: 获取当前文章下的所有评论记录
comments = article.comment_set.all()
# gst: 遍历评论将每条评论的is_enable设为True启用状态
for comment in comments:
comment.is_enable = True
comment.save() # gst: 保存修改后的评论状态
def test_validate_comment(self):
"""
gst: 核心测试用例验证评论相关完整流程
涵盖登录创建测试文章提交评论审核回复标签功能等场景
"""
# gst: 模拟用户登录,使用 setUp 中创建的超级用户账号
self.client.login(username='liangliangyy1', password='liangliangyy1')
# gst: 创建测试分类,用于关联测试文章
category = Category()
category.name = "categoryccc"
category.save() # gst: 保存分类到测试数据库
# gst: 创建测试文章,作为评论的关联对象
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user # gst: 关联文章作者为测试超级用户
article.category = category # gst: 关联文章到测试分类
article.type = 'a'
article.status = 'p' # gst: 设置文章状态为已发布(可评论状态)
article.save() # gst: 保存文章到测试数据库
# gst: 反向解析评论提交接口的URL传入文章ID参数
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# gst: 第一次提交普通评论(无父评论,非回复场景)
response = self.client.post(comment_url,
{
'body': '123ffffffffff' # gst: 评论正文内容
})
# gst: 验证评论提交后是否重定向预期HTTP状态码302
self.assertEqual(response.status_code, 302)
# gst: 重新查询文章,获取最新关联数据
article = Article.objects.get(pk=article.pk)
# gst: 验证未审核的评论是否不显示此时评论is_enable为False
self.assertEqual(len(article.comment_list()), 0)
# gst: 调用辅助方法,启用该文章的所有评论(模拟审核通过)
self.update_article_comment_status(article)
# gst: 验证审核通过后评论是否正常显示预期1条评论
self.assertEqual(len(article.comment_list()), 1)
# gst: 第二次提交普通评论,验证多评论提交功能
response = self.client.post(comment_url,
{
'body': '123ffffffffff', # gst: 重复正文,测试重复提交允许性
})
# gst: 验证第二次提交是否成功重定向
self.assertEqual(response.status_code, 302)
# gst: 启用第二次提交的评论
self.update_article_comment_status(article)
# gst: 重新查询文章,刷新评论数据
article = Article.objects.get(pk=article.pk)
# gst: 验证两次提交的评论是否都正常显示预期2条评论
self.assertEqual(len(article.comment_list()), 2)
# gst: 获取第一条评论的ID作为父评论ID用于测试回复功能
parent_comment_id = article.comment_list()[0].id
# gst: 提交带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
})
#gst: 验证回复评论提交是否成功重定向
self.assertEqual(response.status_code, 302)
self.update_article_comment_status(article)
#gst: 重新查询文章,获取最新评论数据
article = Article.objects.get(pk=article.pk)
#gst: 验证三次提交2 条普通 + 1 条回复)是否都显示(预期 3 条评论)
self.assertEqual(len(article.comment_list()), 3)
#gst: 测试 parse_commenttree 模板标签,获取父评论的子评论链
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
#gst: 验证子评论数量是否正确(预期 1 条回复)
self.assertEqual(len(tree), 1)
#gst: 测试 show_comment_item 模板标签,验证评论项渲染数据
data = show_comment_item(comment, True)
#gst: 验证标签是否返回有效数据(非空)
self.assertIsNotNone(data)
#gst: 测试 get_max_articleid_commentid 工具函数
s = get_max_articleid_commentid()
#gst: 验证工具函数是否返回有效结果(非空)
self.assertIsNotNone(s)
#gst: 测试评论邮件发送功能,导入工具函数并调用
from comments.utils import send_comment_email
send_comment_email(comment)

@ -0,0 +1,17 @@
# gst: 导入Django的path函数用于定义URL路由规则
from django.urls import path
# gst: 导入当前应用comments的视图模块关联URL与视图逻辑
from . import views
# gst: 定义应用命名空间避免不同应用间URL名称冲突
app_name = "comments"
# gst: URL路由配置列表映射URL路径到对应的视图处理类
urlpatterns = [
path(
'article/<int:article_id>/postcomment', # gst: 评论提交URL路径<int:article_id>为文章ID路径参数整数类型
views.CommentPostView.as_view(), # gst: 关联评论提交处理视图(基于类的视图)
name='postcomment' # gst: URL名称用于模板反向解析如reverse('comments:postcomment')
),
]

@ -0,0 +1,65 @@
# gst: 导入日志模块,用于记录函数执行过程中的异常信息
import logging
# gst: 导入国际化翻译工具别名_支持邮件内容多语言显示
from django.utils.translation import gettext_lazy as _
# gst: 导入项目工具函数,用于获取当前站点信息和发送邮件
from djangoblog.utils import get_current_site # 获取当前站点域名
from djangoblog.utils import send_email # 邮件发送工具函数
# gst: 初始化日志器,日志名称为当前模块名(便于定位日志来源)
logger = logging.getLogger(__name__)
def send_comment_email(comment):
"""
gst: 评论邮件通知函数发送两类邮件
1. 给评论者的感谢邮件确认评论提交成功
2. 给父评论者的回复通知邮件告知其评论收到回复
:param comment: 评论对象包含评论者关联文章父评论等信息
"""
# gst: 获取当前站点域名(用于拼接文章访问链接)
site = get_current_site().domain
# gst: 邮件主题(支持国际化翻译)
subject = _('Thanks for your comment')
# gst: 拼接文章的完整访问URLHTTPS协议 + 站点域名 + 文章绝对路径)
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# gst: 构造给评论者的感谢邮件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}
# gst: 获取评论者的邮箱地址(邮件接收人)
tomail = comment.author.email
# gst: 发送感谢邮件接收人列表、主题、HTML内容
send_email([tomail], subject, html_content)
try:
# gst: 判断当前评论是否为回复(存在父评论)
if comment.parent_comment:
# gst: 构造给父评论者的回复通知邮件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/>
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 # 父评论的正文内容
}
# gst: 获取父评论者的邮箱地址(回复通知接收人)
tomail = comment.parent_comment.author.email
# gst: 发送回复通知邮件
send_email([tomail], subject, html_content)
except Exception as e:
# gst: 捕获邮件发送过程中的异常,记录错误日志(不中断程序执行)
logger.error(e)

@ -0,0 +1,91 @@
# gst: 导入Django核心模块用于异常处理、HTTP响应、数据查询等
from django.core.exceptions import ValidationError # 数据验证异常(用于评论关闭时抛出错误)
from django.http import HttpResponseRedirect # 重定向响应类
from django.shortcuts import get_object_or_404 # 查找数据不存在则返回404
from django.utils.decorators import method_decorator # 用于给类视图方法添加装饰器
from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器防止跨站请求伪造
from django.views.generic.edit import FormView # 基于类的表单处理视图(简化表单提交逻辑)
# gst: 导入关联模型和表单,用于视图数据处理
from accounts.models import BlogUser # 用户模型(评论作者)
from blog.models import Article # 文章模型(评论关联对象)
from .forms import CommentForm # 评论表单(用于数据验证和提交)
from .models import Comment # 评论模型(用于保存评论数据)
class CommentPostView(FormView):
"""
gst: 评论提交视图类继承FormView专门处理表单提交的通用视图
负责评论表单的展示数据验证提交保存及跳转逻辑
"""
form_class = CommentForm # gst: 关联的表单类CommentForm用于验证提交数据
template_name = 'blog/article_detail.html' # gst: 表单验证失败时渲染的模板(文章详情页)
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
gst: 重写dispatch方法添加CSRF保护装饰器
确保所有通过该视图的请求都经过CSRF验证防止跨站请求伪造攻击
"""
return super(CommentPostView, self).dispatch(*args, **kwargs) # gst: 调用父类dispatch方法保持原有逻辑
def get(self, request, *args, **kwargs):
"""
gst: 处理GET请求直接访问评论提交URL时
重定向到对应文章详情页的评论区避免直接访问表单提交接口
"""
article_id = self.kwargs['article_id'] # gst: 从URL路径参数中获取文章ID
article = get_object_or_404(Article, pk=article_id) # gst: 查找文章不存在则返回404
url = article.get_absolute_url() # gst: 获取文章的绝对路径详情页URL
return HttpResponseRedirect(url + "#comments") # gst: 重定向到文章详情页的评论区锚点
def form_invalid(self, form):
"""
gst: 表单数据验证失败时的处理逻辑
渲染文章详情页携带错误表单数据展示验证失败信息
"""
article_id = self.kwargs['article_id'] # gst: 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # gst: 获取对应的文章对象
# gst: 返回文章详情页模板,传递错误表单和文章对象(用于页面渲染错误信息)
return self.render_to_response({
'form': form, # 验证失败的表单(含错误信息)
'article': article # 关联的文章对象
})
def form_valid(self, form):
"""
gst: 表单数据验证合法后的核心逻辑
处理评论保存状态设置回复关联等业务最后重定向到评论位置
"""
user = self.request.user # gst: 获取当前登录用户(评论提交者)
author = BlogUser.objects.get(pk=user.pk) # gst: 通过用户ID获取对应的BlogUser对象评论作者
article_id = self.kwargs['article_id'] # gst: 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # gst: 获取评论关联的文章不存在则404
# gst: 检查文章状态评论关闭comment_status='c'或文章草稿status='c')时,禁止评论
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") # gst: 抛出验证错误,终止评论提交
comment = form.save(False) # gst: 表单数据暂存(不立即保存到数据库)
comment.article = article # gst: 关联评论到当前文章
# gst: 获取博客全局设置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review: # gst: 若不需要审核,直接启用评论
comment.is_enable = True
comment.author = author # gst: 关联评论作者
# gst: 处理回复功能若表单提交了父评论ID关联到对应的父评论
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) # gst: 根据ID获取父评论对象
comment.parent_comment = parent_comment # gst: 关联当前评论到父评论
comment.save(True) # gst: 最终保存评论数据到数据库True表示执行完整保存逻辑
# gst: 重定向到文章详情页的当前评论位置(锚点定位到具体评论)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk)) # 拼接URL文章绝对路径 + 评论ID锚点

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save