# 导入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//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等)不通过表单提交, # 而是在视图中通过后端逻辑自动填充(避免用户篡改)