Merge branch 'ZY_branch' into develop

develop
ymq 4 months ago
commit 64ed8af8cf

@ -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'<a href="%s">%s</a>' %
(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'<a href="%s">%s</a>' % (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”显示为“文章”

@ -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"

@ -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无需前端用户输入

@ -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
},
),
]
]

@ -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='是否显示'),
),
]

@ -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

@ -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)

@ -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/<int:article_id>/postcomment',
# URL路径规则
# - 'article/':固定路径前缀,标识与文章相关的操作
# - '<<int:article_id>/'动态路径参数接收整数类型的文章ID用于指定评论所属文章
# - 'postcomment':固定路径后缀,标识“提交评论”的操作
'article/<<int:article_id>/postcomment',
# 关联的视图调用views.py中的CommentPostView类视图的as_view()方法(类视图需转为视图函数)
# 该视图负责处理评论提交的业务逻辑(如数据验证、保存评论等)
views.CommentPostView.as_view(),
name='postcomment'),
]
# URL名称用于反向解析如在模板或代码中通过name='postcomment'生成URL
name='postcomment'
),
]

@ -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') # 邮件主题(支持国际化,多语言环境下自动切换)
# 拼接文章的完整访问URLhttps://域名 + 文章的相对路径通过模型get_absolute_url()获取)
article_url = f"https://{site}{comment.article.get_absolute_url()}"
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}
tomail = comment.author.email
send_email([tomail], subject, html_content)
# 3. 向「当前评论作者」发送感谢邮件
# 构建HTML格式的邮件内容支持超链接%s占位符通过字典传值替换
html_content = _("""<p>Thank you very much for your comments on this site</p >
You can visit %s
to review your comments,
Thank you again!
<br />
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 <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
if comment.parent_comment: # 判断当前评论是否有父评论(即是否是回复)
# 构建回复通知的HTML邮件内容告知父评论作者“你的评论被回复了”
html_content = _("""Your comment on %s<br/> has
received a reply. <br/> %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}
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) # 将错误信息记录到日志(便于后续排查问题,不中断程序运行)

@ -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路径参数中获取文章IDkwargs对应URL中的<int:article_id>
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()
Loading…
Cancel
Save