|
|
# 导入Django核心模块
|
|
|
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 # 从blog应用导入Article模型(评论关联的文章)
|
|
|
|
|
|
|
|
|
# 创建评论模型(继承Django的Model基类,所有数据库模型都需继承此类)
|
|
|
class Comment(models.Model):
|
|
|
# 评论正文:TextField支持长文本,max_length=300限制最大长度为300字符
|
|
|
# '正文'是字段的verbose_name(在后台管理中显示的名称)
|
|
|
body = models.TextField('正文', max_length=300)
|
|
|
|
|
|
# 评论创建时间:DateTimeField存储日期时间
|
|
|
# default=now 表示默认值为当前时间(评论提交时自动记录)
|
|
|
# _('creation time') 是国际化翻译标记(可根据语言设置显示不同文字)
|
|
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
|
|
|
|
# 评论最后修改时间:用于记录评论是否被编辑过
|
|
|
# 初始默认值为创建时间,若后续编辑评论,需手动更新此字段
|
|
|
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
|
|
|
|
|
# 评论作者:关联Django用户模型(外键)
|
|
|
# settings.AUTH_USER_MODEL 是项目配置的用户模型(通常是Django内置User)
|
|
|
# on_delete=models.CASCADE 表示:若用户被删除,其所有评论也会被级联删除
|
|
|
# verbose_name=_('author') 用于后台显示和国际化
|
|
|
author = models.ForeignKey(
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
verbose_name=_('author'),
|
|
|
on_delete=models.CASCADE)
|
|
|
|
|
|
# 关联的文章:外键关联blog应用的Article模型
|
|
|
# 表示“这条评论属于哪篇文章”
|
|
|
# on_delete=models.CASCADE 表示:若文章被删除,其所有评论也会被级联删除
|
|
|
article = models.ForeignKey(
|
|
|
Article,
|
|
|
verbose_name=_('article'),
|
|
|
on_delete=models.CASCADE)
|
|
|
|
|
|
# 父评论:自关联外键,用于实现“评论回复”功能
|
|
|
# 'self' 表示关联当前模型(Comment自身)
|
|
|
# null=True, blank=True 表示可以为空(即顶级评论,不是回复)
|
|
|
# 例如:用户A评论文章(parent_comment为null),用户B回复A的评论(parent_comment指向A的评论)
|
|
|
parent_comment = models.ForeignKey(
|
|
|
'self',
|
|
|
verbose_name=_('parent comment'),
|
|
|
blank=True,
|
|
|
null=True,
|
|
|
on_delete=models.CASCADE)
|
|
|
|
|
|
# 是否启用:布尔值字段,用于控制评论是否显示在前台
|
|
|
# default=False 表示新评论默认不显示(需管理员审核后设为True)
|
|
|
# blank=False, null=False 强制该字段必须有值(不能空)
|
|
|
is_enable = models.BooleanField(_('enable'),
|
|
|
default=False, blank=False, null=False)
|
|
|
|
|
|
# 元数据配置:对模型的补充说明(不影响数据结构,影响Django处理方式)
|
|
|
class Meta:
|
|
|
ordering = ['-id'] # 排序规则:按id倒序(新评论在前,因为id自增)
|
|
|
verbose_name = _('comment') # 模型的单数名称(用于后台显示)
|
|
|
verbose_name_plural = verbose_name # 模型的复数名称(保持和单数一致)
|
|
|
get_latest_by = 'id' # 指定获取“最新记录”时按id字段排序
|
|
|
|
|
|
# 定义模型实例的字符串表示(在后台管理和打印对象时显示)
|
|
|
# 这里返回评论正文的前N个字符,方便识别不同评论
|
|
|
def __str__(self):
|
|
|
return self.body
|
|
|
# 导入Django核心模块
|
|
|
from django.conf import settings # 引入项目配置(如自定义用户模型)
|
|
|
from django.db import models # 引入Django数据库模型基类
|
|
|
from django.utils.timezone import now # 引入当前时间工具(带时区支持)
|
|
|
from django.utils.translation import gettext_lazy as _ # 引入国际化翻译工具(支持多语言)
|
|
|
|
|
|
from blog.models import Article # 从blog应用导入Article模型(评论需关联具体文章)
|
|
|
|
|
|
|
|
|
# 定义评论模型(继承models.Model,所有Django数据库模型必须继承此类)
|
|
|
class Comment(models.Model):
|
|
|
# 评论正文:TextField支持长文本,max_length=300限制最大长度(防止恶意刷屏)
|
|
|
# '正文'是字段在后台管理界面的显示名称
|
|
|
body = models.TextField('正文', max_length=300)
|
|
|
|
|
|
# 评论创建时间:DateTimeField存储日期时间
|
|
|
# default=now 表示默认值为评论提交时的时间(自动记录)
|
|
|
# _('creation time') 用于国际化(如切换语言时显示对应语言的“创建时间”)
|
|
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
|
|
|
|
# 评论最后修改时间:记录评论是否被编辑过
|
|
|
# 初始值为创建时间,若后续编辑评论,需手动更新此字段(可优化为自动更新)
|
|
|
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
|
|
|
|
|
# 评论作者:外键关联用户模型(多对一关系)
|
|
|
# settings.AUTH_USER_MODEL 指向项目配置的用户模型(默认是Django内置的User)
|
|
|
# on_delete=models.CASCADE 表示:若用户账号被删除,其所有评论也会被级联删除
|
|
|
# verbose_name=_('author') 是后台显示名称(支持国际化)
|
|
|
author = models.ForeignKey(
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
verbose_name=_('author'),
|
|
|
on_delete=models.CASCADE)
|
|
|
|
|
|
# 关联的文章:外键关联blog应用的Article模型(多对一关系)
|
|
|
# 表示“这条评论属于哪篇文章”
|
|
|
# on_delete=models.CASCADE 表示:若文章被删除,其所有评论也会被级联删除
|
|
|
article = models.ForeignKey(
|
|
|
Article,
|
|
|
verbose_name=_('article'),
|
|
|
on_delete=models.CASCADE)
|
|
|
|
|
|
# 父评论:自关联外键(评论可以回复其他评论)
|
|
|
# 'self' 表示关联当前模型(Comment自身)
|
|
|
# null=True, blank=True 允许为空(即“顶级评论”,不是回复任何评论)
|
|
|
# 例如:用户A评论文章(parent_comment为null),用户B回复A(parent_comment指向A的评论ID)
|
|
|
parent_comment = models.ForeignKey(
|
|
|
'self',
|
|
|
verbose_name=_('parent comment'),
|
|
|
blank=True,
|
|
|
null=True,
|
|
|
on_delete=models.CASCADE)
|
|
|
|
|
|
# 是否启用:控制评论是否在前台显示(审核机制)
|
|
|
# default=False 表示新评论默认“未启用”(需管理员审核通过后设为True)
|
|
|
# blank=False, null=False 强制该字段必须有值(不能为空)
|
|
|
is_enable = models.BooleanField(_('enable'),
|
|
|
default=False, blank=False, null=False)
|
|
|
|
|
|
# 元数据配置:定义模型的额外属性(不影响数据结构,影响Django处理方式)
|
|
|
class Meta:
|
|
|
ordering = ['-id'] # 排序规则:按id倒序(新评论在前,因为id是自增的)
|
|
|
verbose_name = _('comment') # 模型的单数名称(后台显示用,支持国际化)
|
|
|
verbose_name_plural = verbose_name # 模型的复数名称(保持与单数一致)
|
|
|
get_latest_by = 'id' # 指定“获取最新记录”时按id排序(与ordering一致)
|
|
|
|
|
|
# 定义模型实例的字符串表示(在后台管理、打印对象时显示)
|
|
|
# 返回评论正文,方便快速识别不同评论
|
|
|
def __str__(self):
|
|
|
return self.body
|
|
|
# 导入Django核心模块
|
|
|
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 # 基础表单处理视图(简化表单验证逻辑)
|
|
|
|
|
|
# 导入其他应用模型和当前应用的表单、模型
|
|
|
from accounts.models import BlogUser # 从accounts应用导入用户模型(评论作者)
|
|
|
from blog.models import Article # 从blog应用导入文章模型(评论关联的文章)
|
|
|
from .forms import CommentForm # 导入评论表单(用于验证用户输入)
|
|
|
from .models import Comment # 导入评论模型(用于创建评论数据)
|
|
|
|
|
|
|
|
|
class CommentPostView(FormView):
|
|
|
"""
|
|
|
评论提交视图:处理用户提交的评论,包含表单验证、评论创建、权限判断等逻辑
|
|
|
继承FormView,无需手动编写表单渲染和基础验证代码,专注业务逻辑
|
|
|
"""
|
|
|
form_class = CommentForm # 指定使用的表单类(CommentForm)
|
|
|
template_name = 'blog/article_detail.html' # 表单验证失败时渲染的模板(文章详情页)
|
|
|
|
|
|
@method_decorator(csrf_protect)
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
"""
|
|
|
重写dispatch方法:给视图添加CSRF保护
|
|
|
dispatch是所有请求的入口方法,添加@csrf_protect确保POST请求经过CSRF验证
|
|
|
"""
|
|
|
return super(CommentPostView, self).dispatch(*args, **kwargs)
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
"""
|
|
|
处理GET请求:当用户直接访问评论提交URL时,重定向到文章详情页的评论区
|
|
|
避免用户通过GET方式提交评论(评论应通过POST提交)
|
|
|
"""
|
|
|
article_id = self.kwargs['article_id'] # 从URL参数中获取文章ID
|
|
|
article = get_object_or_404(Article, pk=article_id) # 查询对应的文章
|
|
|
url = article.get_absolute_url() # 获取文章详情页的URL
|
|
|
return HttpResponseRedirect(url + "#comments") # 重定向到文章页的评论区锚点
|
|
|
|
|
|
def form_invalid(self, form):
|
|
|
"""
|
|
|
表单验证失败时的逻辑(如评论内容为空、长度超限等)
|
|
|
重新渲染文章详情页,并传递错误的表单(显示验证错误信息)
|
|
|
"""
|
|
|
article_id = self.kwargs['article_id'] # 获取文章ID
|
|
|
article = get_object_or_404(Article, pk=article_id) # 查询文章
|
|
|
|
|
|
# 渲染文章详情页,携带错误的表单和文章对象(模板中可显示错误信息)
|
|
|
return self.render_to_response({
|
|
|
'form': form, # 验证失败的表单(含错误信息)
|
|
|
'article': article # 文章对象(用于显示文章内容)
|
|
|
})
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
"""
|
|
|
表单验证成功后的核心逻辑:创建评论并保存到数据库
|
|
|
"""
|
|
|
# 获取当前登录用户(评论作者)
|
|
|
user = self.request.user
|
|
|
author = BlogUser.objects.get(pk=user.pk) # 从自定义用户模型中查询用户
|
|
|
|
|
|
# 获取URL参数中的文章ID,并查询对应的文章
|
|
|
article_id = self.kwargs['article_id']
|
|
|
article = get_object_or_404(Article, pk=article_id)
|
|
|
|
|
|
# 检查文章是否允许评论:若文章评论关闭或状态为草稿,则抛出验证错误
|
|
|
if article.comment_status == 'c' or article.status == 'c':
|
|
|
raise ValidationError("该文章评论已关闭.")
|
|
|
|
|
|
# 保存表单数据但不提交到数据库(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']:
|
|
|
parent_comment = Comment.objects.get(
|
|
|
pk=form.cleaned_data['parent_comment_id']
|
|
|
)
|
|
|
comment.parent_comment = parent_comment # 关联到父评论
|
|
|
|
|
|
# 最终保存评论到数据库(commit=True)
|
|
|
comment.save(True)
|
|
|
|
|
|
# 评论提交成功后,重定向到文章详情页的该评论位置(锚点定位)
|
|
|
return HttpResponseRedirect(
|
|
|
"%s#div-comment-%d" % (article.get_absolute_url(), comment.pk)
|
|
|
)
|
|
|
# 导入必要的模块和类
|
|
|
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 # 表单处理基类(简化表单验证流程)
|
|
|
|
|
|
# 导入关联模型和表单
|
|
|
from accounts.models import BlogUser # 自定义用户模型(评论作者作者)
|
|
|
from blog.models import Article # 博客文章模型(评论关联的文章)
|
|
|
from .forms import CommentForm # 评论表单(用于验证用户输入)
|
|
|
from .models import Comment # 评论模型(用于创建和保存评论)
|
|
|
|
|
|
|
|
|
class CommentPostView(FormView):
|
|
|
"""
|
|
|
评论提交视图:处理用户评论的提交、验证、保存逻辑
|
|
|
继承FormView,复用表单渲染、验证等基础功能,专注业务逻辑
|
|
|
"""
|
|
|
form_class = CommentForm # 指定使用的表单类(CommentForm)
|
|
|
template_name = 'blog/article_detail.html' # 表单验证失败时渲染的模板(文章详情页)
|
|
|
|
|
|
@method_decorator(csrf_protect)
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
"""
|
|
|
重写dispatch方法:为视图添加CSRF保护
|
|
|
dispatch是所有请求的入口,确保POST请求经过CSRF验证,防止跨站攻击
|
|
|
"""
|
|
|
return super().dispatch(*args, **kwargs) # 调用父类方法,保持原有逻辑
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
"""
|
|
|
处理GET请求:当用户直接通过URL访问评论提交地址时
|
|
|
重定向到文章详情页的评论区,避免GET方式提交评论(评论需通过POST提交)
|
|
|
"""
|
|
|
article_id = self.kwargs['article_id'] # 从URL参数中获取文章ID
|
|
|
article = get_object_or_404(Article, pk=article_id) # 查询对应的文章
|
|
|
url = article.get_absolute_url() # 获取文章详情页的URL
|
|
|
return HttpResponseRedirect(f"{url}#comments") # 重定向到评论区锚点
|
|
|
|
|
|
def form_invalid(self, form):
|
|
|
"""
|
|
|
表单验证失败时的处理(如评论内容为空、长度超限等)
|
|
|
重新渲染文章详情页,并传递错误的表单,在页面上显示验证错误
|
|
|
"""
|
|
|
article_id = self.kwargs['article_id'] # 获取文章ID
|
|
|
article = get_object_or_404(Article, pk=article_id) # 查询文章
|
|
|
|
|
|
# 渲染文章详情页,携带错误表单和文章对象(模板中可显示错误信息)
|
|
|
return self.render_to_response({
|
|
|
'form': form, # 验证失败的表单(含错误信息)
|
|
|
'article': article # 文章对象(用于显示文章内容)
|
|
|
})
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
"""
|
|
|
表单验证成功后的核心逻辑:创建评论并保存到数据库
|
|
|
"""
|
|
|
# 获取当前登录用户(评论作者)
|
|
|
user = self.request.user
|
|
|
author = BlogUser.objects.get(pk=user.pk) # 从自定义用户模型中查询用户
|
|
|
|
|
|
# 获取URL参数中的文章ID,并查询对应的文章
|
|
|
article_id = self.kwargs['article_id']
|
|
|
article = get_object_or_404(Article, pk=article_id)
|
|
|
|
|
|
# 检查文章是否允许评论:若文章评论关闭(comment_status='c')或状态为草稿(status='c')
|
|
|
if article.comment_status == 'c' or article.status == 'c':
|
|
|
raise ValidationError("该文章评论已关闭.") # 抛出验证错误
|
|
|
|
|
|
# 保存表单数据但不提交到数据库(commit=False),先补充其他字段
|
|
|
comment = form.save(commit=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,则设置为回复
|
|
|
parent_comment_id = form.cleaned_data.get('parent_comment_id')
|
|
|
if parent_comment_id:
|
|
|
parent_comment = Comment.objects.get(pk=parent_comment_id)
|
|
|
comment.parent_comment = parent_comment # 关联到父评论
|
|
|
|
|
|
# 最终保存评论到数据库
|
|
|
comment.save()
|
|
|
|
|
|
# 评论提交成功后,重定向到文章详情页的该评论位置(通过锚点定位)
|
|
|
return HttpResponseRedirect(
|
|
|
f"{article.get_absolute_url()}#div-comment-{comment.pk}"
|
|
|
)
|
|
|
# 导入Django的URL路径处理模块
|
|
|
from django.urls import path
|
|
|
|
|
|
# 导入当前应用的视图模块(views.py)
|
|
|
from . import views
|
|
|
|
|
|
# 定义应用命名空间(app_name),用于在模板中通过命名空间引用URL,避免不同应用的URL名称冲突
|
|
|
app_name = "comments"
|
|
|
|
|
|
# 定义URL路由列表,每个path对应一个视图
|
|
|
urlpatterns = [
|
|
|
# 评论提交的URL路由
|
|
|
path(
|
|
|
'article/<int:article_id>/postcomment', # URL路径规则
|
|
|
views.CommentPostView.as_view(), # 对应的视图类(转换为可调用的视图函数)
|
|
|
name='postcomment' # 路由名称(用于模板中反向解析URL)
|
|
|
),
|
|
|
]
|
|
|
# 导入Django测试工具和核心模块
|
|
|
from django.test import Client, RequestFactory, TransactionTestCase # 测试客户端、请求工厂、事务测试基类
|
|
|
from django.urls import reverse # 用于反向解析URL
|
|
|
|
|
|
# 导入关联模型、模板标签和工具函数
|
|
|
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 # 获取最大文章/评论ID的工具函数
|
|
|
|
|
|
|
|
|
# 定义评论测试类(继承TransactionTestCase,支持事务回滚,避免测试数据污染)
|
|
|
class CommentsTest(TransactionTestCase):
|
|
|
def setUp(self):
|
|
|
"""
|
|
|
测试前的初始化工作:创建测试客户端、测试用户、博客设置等
|
|
|
所有测试方法执行前会自动调用
|
|
|
"""
|
|
|
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() # 保存修改
|
|
|
|
|
|
def test_validate_comment(self):
|
|
|
"""
|
|
|
核心测试方法:验证评论提交、显示、回复等功能的正确性
|
|
|
涵盖正常评论、带格式的回复、评论列表数量等场景
|
|
|
"""
|
|
|
# 1. 登录测试用户
|
|
|
self.client.login(username='liangliangyy1', password='liangliangyy1')
|
|
|
|
|
|
# 2. 创建测试分类和文章(评论必须关联文章)
|
|
|
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' # 文章类型(假设'a'表示普通文章)
|
|
|
article.status = 'p' # 状态(假设'p'表示已发布)
|
|
|
article.save()
|
|
|
|
|
|
# 3. 测试首次提交评论
|
|
|
# 反向解析评论提交URL(使用命名空间和文章ID)
|
|
|
comment_url = reverse(
|
|
|
'comments:postcomment', kwargs={'article_id': article.id})
|
|
|
|
|
|
# 发送POST请求提交评论(内容为'123ffffffffff')
|
|
|
response = self.client.post(comment_url, {'body': '123ffffffffff'})
|
|
|
|
|
|
# 验证:提交成功应重定向(状态码302)
|
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
|
|
# 验证:因评论需要审核(is_enable默认False),评论列表应为空
|
|
|
article = Article.objects.get(pk=article.pk) # 重新查询文章(刷新数据)
|
|
|
self.assertEqual(len(article.comment_list()), 0) # 假设comment_list()返回启用的评论
|
|
|
|
|
|
# 手动启用所有评论(模拟审核通过)
|
|
|
self.update_article_comment_status(article)
|
|
|
# 验证:启用后评论列表数量应为1
|
|
|
self.assertEqual(len(article.comment_list()), 1)
|
|
|
|
|
|
# 4. 测试再次提交评论(验证多条评论的情况)
|
|
|
response = self.client.post(comment_url, {'body': '123ffffffffff'})
|
|
|
self.assertEqual(response.status_code, 302) # 重定向成功
|
|
|
|
|
|
# 启用评论后验证数量为2
|
|
|
article = Article.objects.get(pk=article.pk)
|
|
|
self.update_article_comment_status(article)
|
|
|
self.assertEqual(len(article.comment_list()), 2)
|
|
|
|
|
|
# 5. 测试回复功能(带格式的回复内容)
|
|
|
# 获取第一条评论的ID作为父评论
|
|
|
parent_comment_id = article.comment_list()[0].id
|
|
|
|
|
|
# 提交带格式的回复(包含Markdown语法:标题、代码块、链接)
|
|
|
response = self.client.post(comment_url, {
|
|
|
'body': '''
|
|
|
# Title1
|
|
|
|
|
|
```python
|
|
|
import os
|
|
|
# 导入Django表单基础模块
|
|
|
from django import forms
|
|
|
from django.forms import ModelForm # 导入模型表单基类(可直接关联数据库模型)
|
|
|
|
|
|
# 导入当前应用的评论模型
|
|
|
from .models import Comment
|
|
|
|
|
|
|
|
|
class CommentForm(ModelForm):
|
|
|
"""
|
|
|
评论表单类:继承ModelForm,自动关联Comment模型,简化表单字段定义和验证
|
|
|
用于处理用户提交的评论内容及回复关系
|
|
|
"""
|
|
|
# 自定义字段:父评论ID(用于实现回复功能)
|
|
|
# IntegerField:存储父评论的ID(整数类型)
|
|
|
# widget=forms.HiddenInput:隐藏输入框(不在页面显示,通过前端JS动态设置值)
|
|
|
# required=False:允许为空(表示“顶级评论”,不是回复任何评论)
|
|
|
parent_comment_id = forms.IntegerField(
|
|
|
widget=forms.HiddenInput,
|
|
|
required=False
|
|
|
)
|
|
|
|
|
|
# 元数据配置:关联模型及字段映射
|
|
|
class Meta:
|
|
|
model = Comment # 指定关联的模型(Comment)
|
|
|
fields = ['body'] # 需处理的模型字段(仅包含评论正文body)
|
|
|
# 说明:其他字段(如author、article、creation_time等)不通过表单提交,
|
|
|
# 而是在视图中通过后端逻辑自动填充(避免用户篡改) |