You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tentest/doc/frrweek8work3

480 lines
24 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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