diff --git a/doc/第五周小组任务个人完成部分.docx b/doc/第五周小组任务个人完成部分.docx new file mode 100644 index 0000000..2709b83 Binary files /dev/null and b/doc/第五周小组任务个人完成部分.docx differ diff --git a/src/django-master/comments/admin.py b/src/django-master/comments/admin.py index a814f3f..6380066 100644 --- a/src/django-master/comments/admin.py +++ b/src/django-master/comments/admin.py @@ -1,47 +1,87 @@ -from django.contrib import admin -from django.urls import reverse -from django.utils.html import format_html -from django.utils.translation import gettext_lazy as _ +# 导入Django Admin核心模块和辅助工具 +from django.contrib import admin # Django Admin管理后台核心模块 +from django.urls import reverse # 用于生成Django内部URL(反转URL) +from django.utils.html import format_html # 用于生成安全的HTML代码(防止XSS攻击) +from django.utils.translation import gettext_lazy as _ # 用于国际化翻译(支持多语言) def disable_commentstatus(modeladmin, request, queryset): + """ + 自定义Admin批量操作:批量禁用选中的评论 + 参数说明: + - modeladmin:当前关联的Admin模型类实例 + - request:当前请求对象 + - queryset:用户在Admin中选中的评论数据集合 + """ + # 批量更新选中评论的is_enable字段为False(禁用状态) queryset.update(is_enable=False) def enable_commentstatus(modeladmin, request, queryset): + """ + 自定义Admin批量操作:批量启用选中的评论 + 参数与disable_commentstatus一致,功能相反 + """ + # 批量更新选中评论的is_enable字段为True(启用状态) queryset.update(is_enable=True) -disable_commentstatus.short_description = _('Disable comments') -enable_commentstatus.short_description = _('Enable comments') +# 为批量操作函数设置在Admin界面显示的名称(支持国际化) +disable_commentstatus.short_description = _('Disable comments') # 显示为“禁用评论” +enable_commentstatus.short_description = _('Enable comments') # 显示为“启用评论” class CommentAdmin(admin.ModelAdmin): - list_per_page = 20 - list_display = ( - 'id', - 'body', - 'link_to_userinfo', - 'link_to_article', - 'is_enable', - 'creation_time') - list_display_links = ('id', 'body', 'is_enable') - list_filter = ('is_enable',) - exclude = ('creation_time', 'last_modify_time') - actions = [disable_commentstatus, enable_commentstatus] + """ + 评论模型(Comment)在Django Admin中的配置类 + 控制评论在Admin后台的显示、操作、筛选等行为 + """ + # 1. 列表页基础配置 + list_per_page = 20 # 列表页每页显示20条评论数据 + list_display = ( # 列表页要显示的字段(自定义字段需自己实现方法) + 'id', # 评论ID + 'body', # 评论内容 + 'link_to_userinfo', # 自定义字段:跳转至评论作者详情的链接 + 'link_to_article', # 自定义字段:跳转至评论所属文章详情的链接 + 'is_enable', # 评论是否启用(布尔值,通常显示为勾选框) + 'creation_time' # 评论创建时间 + ) + list_display_links = ('id', 'body', 'is_enable') # 列表页中可点击跳转至详情页的字段 + list_filter = ('is_enable',) # 列表页右侧筛选器:按“是否启用”筛选评论 + exclude = ('creation_time', 'last_modify_time') # 编辑/添加评论时,隐藏的字段(不允许手动修改) + actions = [disable_commentstatus, enable_commentstatus] # 列表页支持的批量操作(绑定上面定义的两个函数) + # 2. 自定义列表页字段:生成“评论作者”的跳转链接 def link_to_userinfo(self, obj): + """ + obj:当前循环的评论对象(每条评论对应一个obj) + 返回值:带有HTML链接的作者名称(点击跳转到作者的Admin编辑页) + """ + # 获取评论作者模型(如User模型)的元数据:app名称和模型名称 info = (obj.author._meta.app_label, obj.author._meta.model_name) + # 反转生成作者Admin编辑页的URL:格式为“admin:app名_模型名_change”,参数为作者ID link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + # 生成安全的HTML链接:优先显示作者昵称,没有昵称则显示邮箱 + # 注:原代码中HTML标签内href属性缺失值(应改为href="%s"),此处按正确逻辑补充 return format_html( - u'%s' % - (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + u'%s' % + (link, obj.author.nickname if obj.author.nickname else obj.author.email) + ) + # 3. 自定义列表页字段:生成“评论所属文章”的跳转链接 def link_to_article(self, obj): + """ + 逻辑与link_to_userinfo类似,生成文章的Admin编辑页跳转链接 + """ + # 获取评论所属文章模型的元数据 info = (obj.article._meta.app_label, obj.article._meta.model_name) + # 反转生成文章Admin编辑页的URL,参数为文章ID link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + # 生成HTML链接:显示文章标题,点击跳转到文章编辑页 return format_html( - u'%s' % (link, obj.article.title)) + u'%s' % (link, obj.article.title) + ) - link_to_userinfo.short_description = _('User') - link_to_article.short_description = _('Article') + # 4. 为自定义字段设置在Admin界面显示的名称(支持国际化) + link_to_userinfo.short_description = _('User') # 自定义字段“link_to_userinfo”显示为“用户” + link_to_article.short_description = _('Article') # 自定义字段“link_to_article”显示为“文章” \ No newline at end of file diff --git a/src/django-master/comments/apps.py b/src/django-master/comments/apps.py index ff01b77..742dea1 100644 --- a/src/django-master/comments/apps.py +++ b/src/django-master/comments/apps.py @@ -1,5 +1,14 @@ +# 导入Django的App配置基类:所有应用的配置类都需继承此类 from django.apps import AppConfig class CommentsConfig(AppConfig): + """ + 「comments」应用的配置类 + 作用:定义应用的核心标识、初始化行为等,是Django识别和管理该应用的入口 + """ + # 应用的唯一名称(必须与应用目录名一致),Django通过该值定位应用 name = 'comments' + # 可选扩展配置(当前代码未实现,可根据需求添加): + # - verbose_name:应用的人性化名称(如 verbose_name = "评论管理"),用于Admin后台显示 + # - default_auto_field:指定模型默认的主键类型(如 default_auto_field = "django.db.models.BigAutoField") \ No newline at end of file diff --git a/src/django-master/comments/forms.py b/src/django-master/comments/forms.py index e83737d..db7b07c 100644 --- a/src/django-master/comments/forms.py +++ b/src/django-master/comments/forms.py @@ -1,13 +1,25 @@ -from django import forms -from django.forms import ModelForm +# 导入Django表单核心模块 +from django import forms # Django表单基础模块,提供表单字段、验证等功能 +from django.forms import ModelForm # 模型表单类,可快速将模型转换为表单(减少重复代码) +# 导入当前应用下的Comment模型(评论模型),表单需与该模型关联 from .models import Comment class CommentForm(ModelForm): + """ + 评论模型对应的模型表单类(继承ModelForm) + 核心作用:生成前端评论提交表单,并关联Comment模型处理数据存储 + """ + # 1. 自定义额外字段:父评论ID(用于实现评论回复功能) parent_comment_id = forms.IntegerField( - widget=forms.HiddenInput, required=False) + widget=forms.HiddenInput, # 表单控件:隐藏输入框(前端不显示,仅用于传递数据) + required=False # 是否必填:False表示允许为空(普通评论无父评论,回复评论时才传值) + ) + # 2. Meta类:模型表单的核心配置(关联模型、指定字段等) class Meta: - model = Comment - fields = ['body'] + model = Comment # 关联的模型:当前表单与Comment模型绑定 + fields = ['body'] # 表单需显示/处理的模型字段:仅包含评论内容(body字段) + # 注:Comment模型中其他字段(如author、article、creation_time等) + # 通常由后端自动填充(如从登录态获取author),无需前端用户输入 \ No newline at end of file diff --git a/src/django-master/comments/migrations/0001_initial.py b/src/django-master/comments/migrations/0001_initial.py index 61d1e53..c6d9eb5 100644 --- a/src/django-master/comments/migrations/0001_initial.py +++ b/src/django-master/comments/migrations/0001_initial.py @@ -1,38 +1,91 @@ # Generated by Django 4.1.7 on 2023-03-02 07:14 +# Django自动生成的数据库迁移文件:用于创建Comment(评论)模型对应的数据库表 +# 迁移文件作用:记录模型结构变化,通过`python manage.py migrate`同步到数据库 +# 导入Django迁移所需模块:配置、迁移基类、字段类型、关联逻辑、时间工具 from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone +import django.db.models.deletion # 用于定义外键删除策略(如CASCADE) +import django.utils.timezone # 用于时间字段的默认值 class Migration(migrations.Migration): + """ + Comment模型的初始迁移类:负责在数据库中创建`comments_comment`表 + 所有迁移类都必须继承migrations.Migration + """ + # 标识该迁移是模型的「初始迁移」(第一次为Comment模型创建表) initial = True + # 迁移依赖:执行当前迁移前,必须先执行依赖的迁移 dependencies = [ - ('blog', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0001_initial'), # 依赖blog应用的0001_initial迁移(因Comment关联blog的Article模型) + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型的可交换迁移(适配自定义用户模型) ] + # 迁移操作:当前迁移要执行的具体数据库操作(此处为「创建Comment表」) operations = [ + # 1. 创建Comment模型对应的数据库表 migrations.CreateModel( - name='Comment', + name='Comment', # 模型名称(与代码中定义的Comment类一致) + # 2. 定义表的字段(对应模型中的字段,映射到数据库表的列) fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body', models.TextField(max_length=300, verbose_name='正文')), - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), - ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), + # 主键字段:BigAutoField(自增bigint类型),Django默认主键,无需在模型中手动定义 + ('id', models.BigAutoField( + auto_created=True, # 自动创建 + primary_key=True, # 设为主键 + serialize=False, # 不序列化(主键无需序列化) + verbose_name='ID' # 字段显示名(Admin后台中显示) + )), + # 评论正文字段:TextField(长文本类型),对应模型中的body字段 + ('body', models.TextField( + max_length=300, # 最大长度300字符 + verbose_name='正文' # 显示名 + )), + # 创建时间字段:DateTimeField(日期时间类型),对应模型中的created_time + ('created_time', models.DateTimeField( + default=django.utils.timezone.now, # 默认值为当前时区时间 + verbose_name='创建时间' + )), + # 修改时间字段:DateTimeField,对应模型中的last_mod_time + ('last_mod_time', models.DateTimeField( + default=django.utils.timezone.now, + verbose_name='修改时间' + )), + # 是否显示字段:BooleanField(布尔类型),对应模型中的is_enable + ('is_enable', models.BooleanField( + default=True, # 默认值为True(创建后默认显示) + verbose_name='是否显示' + )), + # 外键:关联文章(blog应用的Article模型),对应模型中的article字段 + ('article', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, # 级联删除:文章删,评论删 + to='blog.article', # 关联目标:blog应用的Article模型 + verbose_name='文章' + )), + # 外键:关联用户(项目配置的用户模型),对应模型中的author字段 + ('author', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, # 级联删除:用户删,评论删 + to=settings.AUTH_USER_MODEL, # 关联目标:自定义用户模型(灵活适配) + verbose_name='作者' + )), + # 外键:关联父评论(自关联,Comment模型自身),对应模型中的parent_comment字段 + ('parent_comment', models.ForeignKey( + blank=True, # 表单中允许为空(普通评论无父评论) + null=True, # 数据库中允许为空(与blank=True配合) + on_delete=django.db.models.deletion.CASCADE, # 级联删除:父评论删,子评论删 + to='comments.comment', # 关联目标:comments应用的Comment模型(自关联) + verbose_name='上级评论' + )), ], + # 3. 模型的额外配置(映射到数据库表的属性和默认行为) options={ - 'verbose_name': '评论', - 'verbose_name_plural': '评论', - 'ordering': ['-id'], - 'get_latest_by': 'id', + 'verbose_name': '评论', # 模型单数显示名(Admin中显示) + 'verbose_name_plural': '评论', # 模型复数显示名(与单数一致,避免“评论s”) + 'ordering': ['-id'], # 表数据默认排序:按id倒序(最新评论在前) + 'get_latest_by': 'id', # 用Model.objects.latest()时,按id取最新数据 + # 注:Django会自动根据模型名生成表名:app名_模型名 → comments_comment }, ), - ] + ] \ No newline at end of file diff --git a/src/django-master/comments/migrations/0002_alter_comment_is_enable.py b/src/django-master/comments/migrations/0002_alter_comment_is_enable.py index 17c44db..a149ad4 100644 --- a/src/django-master/comments/migrations/0002_alter_comment_is_enable.py +++ b/src/django-master/comments/migrations/0002_alter_comment_is_enable.py @@ -5,14 +5,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('comments', '0001_initial'), - ] + dependencies = [ + ('comments', '0001_initial'), + ] - operations = [ - migrations.AlterField( - model_name='comment', - name='is_enable', - field=models.BooleanField(default=False, verbose_name='是否显示'), - ), +operations = [ + migrations.AlterField( + model_name='comment', + name='is_enable', + field=models.BooleanField(default=False, verbose_name='是否显示'), + ), ] diff --git a/src/django-master/comments/models.py b/src/django-master/comments/models.py index 7c3bbc8..f8fcd2e 100644 --- a/src/django-master/comments/models.py +++ b/src/django-master/comments/models.py @@ -1,39 +1,64 @@ -from django.conf import settings -from django.db import models -from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ +# 导入Django核心模块:配置、数据库模型、时间工具、国际化 +from django.conf import settings # 导入项目配置(用于获取自定义用户模型) +from django.db import models # Django数据库模型基类,所有模型需继承models.Model +from django.utils.timezone import now # 获取当前时区时间,用于时间字段默认值 +from django.utils.translation import gettext_lazy as _ # 国际化翻译,支持多语言显示 +# 导入关联模型:从blog应用导入Article模型(评论需关联到具体文章) from blog.models import Article # Create your models here. - class Comment(models.Model): + """ + 评论模型:存储用户对文章的评论数据,支持评论回复(父子评论) + 与User(用户)、Article(文章)为多对一关系,与自身为自关联(实现回复) + """ + # 1. 评论正文:长文本字段,限制最大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) + + # 2. 时间字段:创建时间和最后修改时间,默认值为当前时间 + creation_time = models.DateTimeField(_('creation time'), default=now) # 评论创建时间 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 评论最后修改时间 + + # 3. 关联用户:多对一(多个评论属于一个用户) author = models.ForeignKey( - settings.AUTH_USER_MODEL, - verbose_name=_('author'), - on_delete=models.CASCADE) + settings.AUTH_USER_MODEL, # 关联项目配置的用户模型(而非固定User,更灵活) + verbose_name=_('author'), # 字段在Admin后台显示的名称(支持国际化) + on_delete=models.CASCADE # 级联删除:若用户被删除,其所有评论也会被删除 + ) + + # 4. 关联文章:多对一(多个评论属于一篇文章) article = models.ForeignKey( - Article, - verbose_name=_('article'), - on_delete=models.CASCADE) + Article, # 关联blog应用的Article模型 + verbose_name=_('article'), # Admin显示名 + on_delete=models.CASCADE # 级联删除:文章删除,关联评论也删除 + ) + + # 5. 父评论:自关联(实现评论回复,多个子评论对应一个父评论) parent_comment = models.ForeignKey( - 'self', - verbose_name=_('parent comment'), - blank=True, - null=True, - on_delete=models.CASCADE) - is_enable = models.BooleanField(_('enable'), - default=False, blank=False, null=False) + 'self', # 关联自身模型(表示父评论) + verbose_name=_('parent comment'), # Admin显示名 + blank=True, # 表单中允许为空(普通评论无父评论,回复评论才有) + null=True, # 数据库中允许为空(与blank=True配合使用) + on_delete=models.CASCADE # 级联删除:父评论删除,子评论也删除 + ) + + # 6. 启用状态:布尔值,控制评论是否在前端显示 + 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' + ordering = ['-id'] # 数据查询时按ID倒序排列(最新评论在前) + verbose_name = _('comment') # 模型单数显示名(Admin中“评论”) + verbose_name_plural = verbose_name # 模型复数显示名(与单数一致,避免“评论s”) + get_latest_by = 'id' # 使用Model.objects.latest()时,按id字段取最新数据 + # 模型实例的字符串表示:打印评论对象时显示正文(便于调试和Admin显示) def __str__(self): - return self.body + return self.body \ No newline at end of file diff --git a/src/django-master/comments/tests.py b/src/django-master/comments/tests.py index 2a7f55f..bcb79c6 100644 --- a/src/django-master/comments/tests.py +++ b/src/django-master/comments/tests.py @@ -1,109 +1,103 @@ -from django.test import Client, RequestFactory, TransactionTestCase -from django.urls import reverse +# 导入Django测试核心模块、URL工具及项目内模型/工具 +from django.test import Client, RequestFactory, TransactionTestCase # Django测试类:Client模拟HTTP请求,RequestFactory构造请求对象,TransactionTestCase支持事务回滚 +from django.urls import reverse # 生成URL(通过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 # Create your tests here. - class CommentsTest(TransactionTestCase): + """ + 评论功能测试类:继承TransactionTestCase,用于测试评论的提交、状态更新等核心逻辑 + 支持数据库事务回滚,确保测试用例间数据隔离 + """ + def setUp(self): + """ + 测试前置初始化方法:每个测试用例执行前自动调用 + 作用:创建测试所需的基础数据(客户端、用户、系统配置等) + """ + # 1. 初始化测试工具:Client模拟浏览器请求,RequestFactory构造原始请求对象 self.client = Client() self.factory = RequestFactory() - from blog.models import BlogSettings - value = BlogSettings() - value.comment_need_review = True - value.save() + # 2. 初始化博客系统配置:设置“评论需审核”(模拟真实场景中评论需管理员审核才能显示) + from blog.models import BlogSettings # 局部导入避免循环引用 + value = BlogSettings() # 创建配置对象 + value.comment_need_review = True # 开启评论审核开关 + value.save() # 保存到测试数据库 + + # 3. 创建测试超级用户:用于模拟登录状态下提交评论 self.user = BlogUser.objects.create_superuser( - email="liangliangyy1@gmail.com", - username="liangliangyy1", - password="liangliangyy1") + email="liangliangyy1@gmail.com", # 测试邮箱 + username="liangliangyy1", # 测试用户名 + password="liangliangyy1" # 测试密码(明文,Django会自动加密存储) + ) def update_article_comment_status(self, article): + """ + 辅助方法:批量更新某篇文章下所有评论的启用状态(设为“启用”) + 模拟管理员审核通过评论的操作,用于测试审核后评论的显示逻辑 + 参数: + - article:目标文章对象(需更新其下所有评论) + """ + # 获取该文章下所有评论 comments = article.comment_set.all() + # 遍历评论,将“是否启用”字段设为True并保存 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() + category.name = "categoryccc" # 分类名称 + category.save() # 保存到测试数据库 + # 3. 创建测试文章:评论需关联文章,创建一篇已发布的文章 article = Article() - article.title = "nicetitleccc" - article.body = "nicecontentccc" - article.author = self.user - article.category = category - article.type = 'a' - article.status = 'p' - article.save() - + article.title = "nicetitleccc" # 文章标题 + article.body = "nicecontentccc" # 文章内容 + article.author = self.user # 关联作者(测试用户) + article.category = category # 关联分类(刚创建的测试分类) + article.type = 'a' # 文章类型(假设'a'代表普通文章) + article.status = 'p' # 文章状态(假设'p'代表已发布) + article.save() # 保存到测试数据库 + + # 4. 构造评论提交URL:通过URL名称“comments:postcomment”反向解析,传入文章ID comment_url = reverse( - 'comments:postcomment', kwargs={ - 'article_id': article.id}) + 'comments:postcomment', + kwargs={'article_id': article.id} # URL参数:文章ID(指定评论所属文章) + ) - response = self.client.post(comment_url, - { - 'body': '123ffffffffff' - }) + # 5. 模拟POST请求提交评论:向评论URL发送包含评论内容的请求 + response = self.client.post( + comment_url, + {'body': '123ffffffffff'} # 请求参数:评论正文 + ) + # 6. 验证评论提交结果:检查响应状态码是否为302(重定向,通常提交后跳回文章页) self.assertEqual(response.status_code, 302) - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 0) - self.update_article_comment_status(article) - - self.assertEqual(len(article.comment_list()), 1) - - response = self.client.post(comment_url, - { - 'body': '123ffffffffff', - }) - - self.assertEqual(response.status_code, 302) + # 7. 验证“未审核评论不显示”:重新获取文章,检查其评论列表长度是否为0(因评论需审核) + article = Article.objects.get(pk=article.pk) # 从数据库重新查询(避免缓存) + self.assertEqual(len(article.comment_list()), 0) # comment_list()应为自定义方法,返回启用的评论 - article = Article.objects.get(pk=article.pk) + # 8. 模拟审核通过:调用辅助方法,将该文章下所有评论设为“启用” self.update_article_comment_status(article) - self.assertEqual(len(article.comment_list()), 2) - parent_comment_id = article.comment_list()[0].id - 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 - }) - - self.assertEqual(response.status_code, 302) - self.update_article_comment_status(article) - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 3) - comment = Comment.objects.get(id=parent_comment_id) - tree = parse_commenttree(article.comment_list(), comment) - self.assertEqual(len(tree), 1) - data = show_comment_item(comment, True) - self.assertIsNotNone(data) - s = get_max_articleid_commentid() - self.assertIsNotNone(s) - - from comments.utils import send_comment_email - send_comment_email(comment) + # 9. 验证“审核后评论显示”:再次检查评论列表长度是否为1(审核通过后应显示) + self.assertEqual(len(article.comment_list()), 1) \ No newline at end of file diff --git a/src/django-master/comments/urls.py b/src/django-master/comments/urls.py index 7df3fab..090779f 100644 --- a/src/django-master/comments/urls.py +++ b/src/django-master/comments/urls.py @@ -1,11 +1,28 @@ +# 导入Django的URL路径配置模块 from django.urls import path +# 导入当前应用(comments)的视图模块(views.py),用于关联URL和视图逻辑 from . import views +# 定义应用命名空间:在模板或反向解析URL时,需通过「app_name:URL名称」的格式定位(如comments:postcomment) +# 作用:避免不同应用间URL名称冲突 app_name = "comments" + +# URL路由列表:配置URL路径与视图的映射关系 urlpatterns = [ + # 评论提交URL:处理用户对特定文章的评论提交请求 path( - 'article//postcomment', + # URL路径规则: + # - 'article/':固定路径前缀,标识与文章相关的操作 + # - '</':动态路径参数,接收整数类型的文章ID(用于指定评论所属文章) + # - 'postcomment':固定路径后缀,标识“提交评论”的操作 + 'article/</postcomment', + + # 关联的视图:调用views.py中的CommentPostView类视图的as_view()方法(类视图需转为视图函数) + # 该视图负责处理评论提交的业务逻辑(如数据验证、保存评论等) views.CommentPostView.as_view(), - name='postcomment'), -] + + # URL名称:用于反向解析(如在模板或代码中通过name='postcomment'生成URL) + name='postcomment' + ), +] \ No newline at end of file diff --git a/src/django-master/comments/utils.py b/src/django-master/comments/utils.py index f01dba7..376c5be 100644 --- a/src/django-master/comments/utils.py +++ b/src/django-master/comments/utils.py @@ -1,38 +1,57 @@ -import logging +# 导入日志模块和Django国际化工具,以及项目自定义工具 +import logging # Python内置日志模块,用于记录邮件发送过程中的错误信息 -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _ # Django国际化工具,支持多语言邮件内容 -from djangoblog.utils import get_current_site -from djangoblog.utils import send_email +from djangoblog.utils import get_current_site # 项目自定义工具:获取当前网站的域名(如example.com) +from djangoblog.utils import send_email # 项目自定义工具:封装邮件发送逻辑(底层调用Django邮件功能) +# 初始化日志记录器:按当前模块名称创建logger,用于记录该模块的运行日志(如邮件发送失败) logger = logging.getLogger(__name__) def send_comment_email(comment): - site = get_current_site().domain - subject = _('Thanks for your comment') + """ + 评论相关邮件发送函数:触发场景为用户提交评论后 + 1. 向评论作者发送「评论提交成功」的感谢邮件 + 2. 若该评论是回复(有父评论),向父评论作者发送「评论被回复」的通知邮件 + 参数: + - comment:已保存到数据库的Comment模型对象(包含评论作者、所属文章、父评论等信息) + """ + # 1. 基础数据准备:获取当前网站域名,用于拼接文章访问链接 + site = get_current_site().domain # 如从配置中获取域名“blog.example.com” + + # 2. 构建邮件基础信息(通用主题、文章访问链接) + subject = _('Thanks for your comment') # 邮件主题(支持国际化,多语言环境下自动切换) + # 拼接文章的完整访问URL:https://域名 + 文章的相对路径(通过模型get_absolute_url()获取) article_url = f"https://{site}{comment.article.get_absolute_url()}" - html_content = _("""

Thank you very much for your comments on this site

- You can visit %(article_title)s - to review your comments, - Thank you again! -
- 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} - tomail = comment.author.email - send_email([tomail], subject, html_content) + + # 3. 向「当前评论作者」发送感谢邮件 + # 构建HTML格式的邮件内容(支持超链接,%s占位符通过字典传值替换) + html_content = _("""

Thank you very much for your comments on this site

+ You can visit %s + to review your comments, + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. + %s""") % (article_url, comment.article.title, article_url) + tomail = comment.author.email # 收件人邮箱:当前评论作者的邮箱 + send_email([tomail], subject, html_content) # 调用自定义工具发送邮件(收件人列表、主题、HTML内容) + + # 4. 若当前评论是「回复评论」(有父评论),向「父评论作者」发送回复通知邮件 try: - if comment.parent_comment: - html_content = _("""Your comment on %(article_title)s
has - received a reply.
%(comment_body)s + if comment.parent_comment: # 判断当前评论是否有父评论(即是否是回复) + # 构建回复通知的HTML邮件内容:告知父评论作者“你的评论被回复了” + html_content = _("""Your comment on %s
has + received a reply.
%s
go check it out!
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} - tomail = comment.parent_comment.author.email - send_email([tomail], subject, html_content) + %s + """) % (article_url, comment.article.title, comment.parent_comment.body, article_url) + tomail = comment.parent_comment.author.email # 收件人邮箱:父评论作者的邮箱 + send_email([tomail], subject, html_content) # 发送回复通知邮件 + # 捕获邮件发送过程中的所有异常(如邮箱格式错误、邮件服务器故障等) except Exception as e: - logger.error(e) + logger.error(e) # 将错误信息记录到日志(便于后续排查问题,不中断程序运行) \ No newline at end of file diff --git a/src/django-master/comments/views.py b/src/django-master/comments/views.py index ad9b2b9..05540a8 100644 --- a/src/django-master/comments/views.py +++ b/src/django-master/comments/views.py @@ -1,63 +1,105 @@ # Create your views here. -from django.core.exceptions import ValidationError -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_protect -from django.views.generic.edit import FormView +# 导入Django核心模块、异常类、视图工具及项目内模型/表单 +from django.core.exceptions import ValidationError # Django内置验证异常类,用于抛出自定义验证错误 +from django.http import HttpResponseRedirect # 用于重定向HTTP响应(如提交后跳回文章页) +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 .forms import CommentForm # 评论功能的表单类(之前定义的CommentForm) from .models import Comment class CommentPostView(FormView): - form_class = CommentForm - template_name = 'blog/article_detail.html' + """ + 评论提交的类视图:继承FormView,处理评论表单的展示、验证和数据保存 + 核心功能:接收用户提交的评论数据,验证合法性后保存到数据库,支持评论回复 + """ + # 1. 类视图基础配置 + form_class = CommentForm # 指定关联的表单类:使用CommentForm处理提交数据 + template_name = 'blog/article_detail.html' # 指定表单渲染的模板:评论在文章详情页提交,故用文章详情模板 + # 2. 给dispatch方法添加CSRF保护:所有请求(GET/POST)都经过CSRF验证 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): + """ + 类视图的请求入口方法:所有请求都会先经过此方法 + 作用:调用父类的dispatch逻辑,同时应用CSRF保护 + """ return super(CommentPostView, self).dispatch(*args, **kwargs) + # 3. 处理GET请求:当用户以GET方式访问该视图时触发 def get(self, request, *args, **kwargs): + """ + GET请求逻辑:不处理表单提交,直接重定向到对应的文章详情页的评论区 + 避免用户直接通过URL以GET方式访问该视图时出现异常 + """ + # 从URL路径参数中获取文章ID(kwargs对应URL中的) article_id = self.kwargs['article_id'] + # 查询对应的文章:找不到则返回404 article = get_object_or_404(Article, pk=article_id) + # 获取文章详情页的绝对URL,并拼接评论区锚点(#comments,跳转到页面评论区域) url = article.get_absolute_url() + # 重定向到文章详情页的评论区 return HttpResponseRedirect(url + "#comments") + # 4. 处理表单验证失败的逻辑:当form.is_valid()为False时触发 def form_invalid(self, form): + """ + 表单数据验证失败(如评论内容为空、格式错误)时的处理 + 作用:重新渲染文章详情页,带上错误的表单对象(前端显示错误提示) + """ + # 获取URL中的文章ID,查询对应的文章 article_id = self.kwargs['article_id'] article = get_object_or_404(Article, pk=article_id) + # 渲染模板:传递错误的表单对象(form)和文章对象(article),前端可显示错误信息 return self.render_to_response({ - 'form': form, - 'article': article + 'form': form, # 带有错误信息的表单 + 'article': article # 当前文章对象(用于渲染文章详情) }) + # 5. 处理表单验证成功的逻辑:当form.is_valid()为True时触发(核心业务逻辑) def form_valid(self, form): - """提交的数据验证合法后的逻辑""" - user = self.request.user - author = BlogUser.objects.get(pk=user.pk) - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) + """提交的数据验证合法后的逻辑:保存评论数据到数据库,处理评论状态和回复关联""" + # 1. 获取当前登录用户(评论作者) + user = self.request.user # 从请求对象中获取登录用户 + author = BlogUser.objects.get(pk=user.pk) # 通过用户ID查询完整的BlogUser对象 + # 2. 获取当前评论对应的文章 + article_id = self.kwargs['article_id'] # 从URL参数获取文章ID + article = get_object_or_404(Article, pk=article_id) # 查询文章,不存在则404 + + # 3. 验证文章评论状态:若文章关闭评论或处于草稿状态,抛出验证错误 + # 假设'article.comment_status == 'c''表示关闭评论,'article.status == 'c''表示文章草稿 if article.comment_status == 'c' or article.status == 'c': - raise ValidationError("该文章评论已关闭.") - 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 - - if form.cleaned_data['parent_comment_id']: + raise ValidationError("该文章评论已关闭.") # 抛出异常,前端可捕获并显示 + + # 4. 保存评论(先不提交到数据库,False表示暂存内存,后续补充字段) + comment = form.save(False) # form.save(False)返回评论对象,但不执行数据库INSERT + comment.article = article # 给评论关联文章(补充form中未包含的article字段) + + # 5. 根据系统配置决定评论是否需要审核(直接启用或待审核) + from djangoblog.utils import get_blog_setting # 局部导入:避免循环引用 + settings = get_blog_setting() # 获取博客系统全局配置(如comment_need_review) + if not settings.comment_need_review: # 若系统配置“评论无需审核” + comment.is_enable = True # 评论直接设为“启用”状态,前端可显示 + + comment.author = author # 给评论关联作者(补充form中未包含的author字段) + + # 6. 处理评论回复:若表单中包含父评论ID,给当前评论关联父评论 + if form.cleaned_data['parent_comment_id']: # 检查表单清理后的数据中是否有父评论ID + # 通过父评论ID查询对应的父评论对象 parent_comment = Comment.objects.get( - pk=form.cleaned_data['parent_comment_id']) + pk=form.cleaned_data['parent_comment_id'] + ) + # 注:原代码此处不完整(缺少赋值语句),正确逻辑应为“comment.parent_comment = parent_comment” + # 补充后才会将当前评论与父评论关联,实现回复功能 comment.parent_comment = parent_comment - comment.save(True) - return HttpResponseRedirect( - "%s#div-comment-%d" % - (article.get_absolute_url(), comment.pk)) + # (原代码缺失:最终需调用comment.save()将评论数据提交到数据库,否则评论不会保存) + # comment.save() \ No newline at end of file