Merge branch 'JYN_branch' of https://bdgit.educoder.net/pn39ysei2/project into JYN_branch

JYN_branch
ymq 3 months ago
commit 5de28e464b

@ -4,7 +4,7 @@ from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# 导入自定义用户模型
# jyn:导入自定义用户模型
from .models import BlogUser
@ -13,15 +13,15 @@ class BlogUserCreationForm(forms.ModelForm):
自定义用户创建表单用于在管理员界面添加新用户
继承自ModelForm提供密码验证功能
"""
# 密码字段使用PasswordInput小部件确保输入不可见
# jyn:密码字段使用PasswordInput小部件确保输入不可见
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
# 确认密码字段,用于验证两次输入的密码是否一致
# jyn:确认密码字段,用于验证两次输入的密码是否一致
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
# 指定关联的模型
# jyn:指定关联的模型
model = BlogUser
# 表单中包含的字段,这里只显示邮箱
# jyn:表单中包含的字段,这里只显示邮箱
fields = ('email',)
def clean_password2(self):
@ -31,7 +31,7 @@ class BlogUserCreationForm(forms.ModelForm):
"""
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
# 检查密码是否存在且不一致
# jyn:检查密码是否存在且不一致
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
@ -41,12 +41,12 @@ class BlogUserCreationForm(forms.ModelForm):
重写保存方法确保密码以哈希形式存储
而不是明文存储
"""
# 先调用父类方法获取用户对象,但不立即保存到数据库
# jyn:先调用父类方法获取用户对象,但不立即保存到数据库
user = super().save(commit=False)
# 使用set_password方法对密码进行哈希处理
# jyn:使用set_password方法对密码进行哈希处理
user.set_password(self.cleaned_data["password1"])
if commit:
# 标记用户来源为管理员站点
# jyn:标记用户来源为管理员站点
user.source = 'adminsite'
user.save()
return user
@ -58,11 +58,11 @@ class BlogUserChangeForm(UserChangeForm):
继承自Django内置的UserChangeForm
"""
class Meta:
# 指定关联的模型
# jyn:指定关联的模型
model = BlogUser
# 显示所有字段
# jyn:显示所有字段
fields = '__all__'
# 指定用户名字段的处理类
# jyn:指定用户名字段的处理类
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
@ -75,11 +75,11 @@ class BlogUserAdmin(UserAdmin):
自定义用户管理员类用于在Django管理后台配置用户模型的显示和操作
继承自Django内置的UserAdmin
"""
# 指定编辑用户时使用的表单
# jyn:指定编辑用户时使用的表单
form = BlogUserChangeForm
# 指定添加用户时使用的表单
# jyn:指定添加用户时使用的表单
add_form = BlogUserCreationForm
# 列表页面显示的字段
# jyn:列表页面显示的字段
list_display = (
'id',
'nickname',
@ -89,7 +89,7 @@ class BlogUserAdmin(UserAdmin):
'date_joined',
'source'
)
# 列表页面中可点击跳转的字段
# jyn:列表页面中可点击跳转的字段
list_display_links = ('id', 'username')
# 排序方式按id降序排列最新的用户在前
# jyn:排序方式按id降序排列最新的用户在前
ordering = ('-id',)

@ -8,6 +8,6 @@ class AccountsConfig(AppConfig):
每个Django应用都需要一个配置类用于设置应用的各种属性和行为
通常放在应用目录下的apps.py文件中
"""
# 应用的名称,必须与应用目录名一致
# 这个名称会被Django用来识别和管理应用
# jyn:应用的名称,必须与应用目录名一致
# jyn:这个名称会被Django用来识别和管理应用
name = 'accounts'

@ -4,8 +4,8 @@ from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils # 导入自定义工具模块,可能用于验证码验证等功能
from .models import BlogUser # 导入自定义用户模型
from . import utils # jyn:导入自定义工具模块,可能用于验证码验证等功能
from .models import BlogUser # jyn:导入自定义用户模型
class LoginForm(AuthenticationForm):
@ -14,14 +14,14 @@ class LoginForm(AuthenticationForm):
用于处理用户登录验证主要扩展了表单字段的样式
"""
def __init__(self, *args, **kwargs):
# 调用父类构造方法初始化表单
# jyn:调用父类构造方法初始化表单
super(LoginForm, self).__init__(*args, **kwargs)
# 自定义用户名输入框添加占位符和CSS类
# jyn:自定义用户名输入框添加占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 自定义密码输入框添加占位符和CSS类
# jyn:自定义密码输入框添加占位符和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
@ -32,10 +32,10 @@ class RegisterForm(UserCreationForm):
用于处理用户注册逻辑包含用户名邮箱和密码验证
"""
def __init__(self, *args, **kwargs):
# 调用父类构造方法初始化表单
# jyn:调用父类构造方法初始化表单
super(RegisterForm, self).__init__(*args, **kwargs)
# 自定义各字段的输入控件,添加样式和占位符
# jyn:自定义各字段的输入控件,添加样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
@ -51,14 +51,14 @@ class RegisterForm(UserCreationForm):
表单验证机制中以clean_为前缀的方法会自动被调用
"""
email = self.cleaned_data['email']
# 检查该邮箱是否已存在于数据库中
# jyn:检查该邮箱是否已存在于数据库中
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) # 抛出验证错误
raise ValidationError(_("email already exists")) #jyn: 抛出验证错误
return email
class Meta:
model = get_user_model() # 使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # 注册表单中显示的字段
model = get_user_model() # jyn:使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # jyn:注册表单中显示的字段
class ForgetPasswordForm(forms.Form):
@ -66,7 +66,7 @@ class ForgetPasswordForm(forms.Form):
忘记密码表单用于用户重置密码的流程
包含新密码确认密码邮箱和验证码字段
"""
# 新密码字段
# jyn:新密码字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -77,7 +77,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 确认新密码字段
# jyn:确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -88,7 +88,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 邮箱字段(用于验证用户身份)
# jyn:邮箱字段(用于验证用户身份)
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -99,7 +99,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 验证码字段(用于身份验证)
# jyn:验证码字段(用于身份验证)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -115,11 +115,11 @@ class ForgetPasswordForm(forms.Form):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
# 检查两次密码是否一致
# jyn:检查两次密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
# 使用Django内置的密码验证器验证密码强度
# jyn:使用Django内置的密码验证器验证密码强度
password_validation.validate_password(password2)
return password2
@ -127,22 +127,22 @@ class ForgetPasswordForm(forms.Form):
def clean_email(self):
"""验证邮箱是否已注册"""
user_email = self.cleaned_data.get("email")
# 检查该邮箱是否存在于系统中
# jyn:检查该邮箱是否存在于系统中
if not BlogUser.objects.filter(email=user_email).exists():
# 提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露)
# jyn:提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露)
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
"""验证验证码是否有效"""
code = self.cleaned_data.get("code")
# 调用工具函数验证邮箱和验证码是否匹配
# jyn:调用工具函数验证邮箱和验证码是否匹配
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error) # 验证码无效时抛出错误
raise ValidationError(error) # jyn:验证码无效时抛出错误
return code

@ -3,28 +3,28 @@ from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具函数
from djangoblog.utils import get_current_site # jyn:导入获取当前站点信息的工具函数
# Create your models here.
class BlogUser(AbstractUser):
"""
自定义用户模型继承自Django内置的AbstractUser
jyn:自定义用户模型继承自Django内置的AbstractUser
扩展了默认用户模型的字段以满足博客系统的特定需求
"""
# 昵称字段,允许为空
# jyn:昵称字段,允许为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 账号创建时间,默认为当前时间
# jyn:账号创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间,默认为当前时间(通常需要在保存时更新)
# jyn:最后修改时间,默认为当前时间(通常需要在保存时更新)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道
# jyn:账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""
返回用户详情页的URL
jyn:返回用户详情页的URL
Django推荐为模型定义此方法用于获取对象的标准URL
"""
return reverse(
@ -33,23 +33,23 @@ class BlogUser(AbstractUser):
)
def __str__(self):
"""模型的字符串表示,这里返回用户的邮箱"""
"""jyn:模型的字符串表示,这里返回用户的邮箱"""
return self.email
def get_full_url(self):
"""获取用户详情页的完整URL包含域名"""
# 获取当前站点的域名
"""jyn:获取用户详情页的完整URL包含域名"""
# jyn:获取当前站点的域名
site = get_current_site().domain
# 拼接完整URL协议+域名+路径)
# jyn:拼接完整URL协议+域名+路径)
url = "https://{site}{path}".format(
site=site,
path=self.get_absolute_url() # 调用get_absolute_url获取相对路径
path=self.get_absolute_url() # jyn:调用get_absolute_url获取相对路径
)
return url
class Meta:
"""模型的元数据配置"""
ordering = ['-id'] # 默认排序方式按id降序最新创建的用户在前
verbose_name = _('user') # 模型的单数显示名称(支持国际化)
verbose_name_plural = verbose_name # 模型的复数显示名称(与单数相同)
get_latest_by = 'id' # 指定使用id字段获取最新对象用于Model.objects.latest()
"""jyn:模型的元数据配置"""
ordering = ['-id'] # jyn:默认排序方式按id降序最新创建的用户在前
verbose_name = _('user') # jyn:模型的单数显示名称(支持国际化)
verbose_name_plural = verbose_name # jyn:模型的复数显示名称(与单数相同)
get_latest_by = 'id' # jyn:指定使用id字段获取最新对象用于Model.objects.latest()

@ -3,10 +3,10 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser # 导入自定义用户模型
from blog.models import Article, Category # 导入博客相关模型
from djangoblog.utils import * # 导入项目工具函数
from . import utils # 导入当前应用的工具函数
from accounts.models import BlogUser # jyn:导入自定义用户模型
from blog.models import Article, Category # jyn:导入博客相关模型
from djangoblog.utils import * # jyn:导入项目工具函数
from . import utils # jyn:导入当前应用的工具函数
# Create your tests here.
@ -21,68 +21,68 @@ class AccountTest(TestCase):
测试前的初始化方法会在每个测试方法执行前运行
用于创建测试所需的基础数据
"""
self.client = Client() # 创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象
# 创建一个测试用户
self.client = Client() # jyn:创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # jyn:创建请求工厂,用于构建请求对象
# jyn:创建一个测试用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # 测试用的新密码
self.new_test = "xxx123--=" #jyn: 测试用的新密码
def test_validate_account(self):
"""测试用户账号验证相关功能,包括登录和管理员权限"""
site = get_current_site().domain # 获取当前站点域名
site = get_current_site().domain # jyn:获取当前站点域名
# 创建一个超级用户
# jyn:创建一个超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
# 获取创建的用户
# jyn:获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
# 测试登录功能
# jyn:测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) # 断言登录成功
self.assertEqual(loginresult, True) #jyn: 断言登录成功
# 测试访问管理员页面
# jyn:测试访问管理员页面
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) # 断言页面访问成功
self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
# 创建测试分类
# jyn:创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
# jyn:创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' # 假设'a'表示文章类型
article.status = 'p' # 假设'p'表示已发布
article.type = 'a' # jyn:假设'a'表示文章类型
article.status = 'p' # jyn:假设'p'表示已发布
article.save()
# 测试访问文章管理页面
# jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) # 断言页面访问成功
self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
def test_validate_register(self):
"""测试用户注册功能,包括注册流程、邮箱验证和权限控制"""
# 初始状态下,该邮箱应不存在
# jyn:初始状态下,该邮箱应不存在
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 模拟用户注册提交
# jyn:模拟用户注册提交
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
@ -90,43 +90,43 @@ class AccountTest(TestCase):
'password2': 'password123!q@wE#R$T',
})
# 注册后,该邮箱应存在
#jyn: 注册后,该邮箱应存在
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 获取刚注册的用户
#jyn: 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成验证链接(模拟邮箱验证流程)
# jyn:生成验证链接(模拟邮箱验证流程)
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# 访问验证链接
# jyn:访问验证链接
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # 断言验证页面访问成功
self.assertEqual(response.status_code, 200) # jyn:断言验证页面访问成功
# 登录新注册用户
# jyn:登录新注册用户
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 提升用户权限
# jyn:提升用户权限
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
# 清除缓存
# jyn:清除缓存
delete_sidebar_cache()
# 创建测试分类
# jyn:创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
# jyn:创建测试文章
article = Article()
article.category = category
article.title = "nicetitle333"
@ -136,94 +136,94 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
# 测试访问文章管理页面
# jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试登出功能
# jyn:测试登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) # 登出通常是重定向
# 登出后访问管理页面(应被拒绝或重定向)
# jyn:登出后访问管理页面(应被拒绝或重定向)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 使用错误密码登录
# jyn:使用错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
# 错误登录后访问管理页面
# jyn:错误登录后访问管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""测试邮箱验证码验证功能"""
to_email = "admin@admin.com"
code = generate_code() # 生成验证码
utils.set_code(to_email, code) # 存储验证码
utils.send_verify_email(to_email, code) # 发送验证邮件
code = generate_code() # jyn:生成验证码
utils.set_code(to_email, code) # jyn:存储验证码
utils.send_verify_email(to_email, code) # jyn:发送验证邮件
# 验证正确的邮箱和验证码
# jyn:验证正确的邮箱和验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # 应无错误
self.assertEqual(err, None) # jyn:应无错误
# 验证错误的邮箱和正确的验证码
# jyn:验证错误的邮箱和正确的验证码
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # 应返回错误信息
self.assertEqual(type(err), str) # jyn:应返回错误信息
def test_forget_password_email_code_success(self):
"""测试发送密码重置验证码成功的情况"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com") # 使用已存在的邮箱
data=dict(email="admin@admin.com") # jyn:使用已存在的邮箱
)
self.assertEqual(resp.status_code, 200) # 断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功信息
self.assertEqual(resp.status_code, 200) # jyn:断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # jyn:断言返回成功信息
def test_forget_password_email_code_fail(self):
"""测试发送密码重置验证码失败的情况"""
# 不提供邮箱
# jyn:不提供邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # jyn:断言返回错误信息
# 提供无效格式的邮箱
# jyn:提供无效格式的邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #jyn: 断言返回错误信息
def test_forget_password_email_success(self):
"""测试密码重置成功的情况"""
code = generate_code() # 生成验证码
utils.set_code(self.blog_user.email, code) # 存储验证码
# 准备重置密码的数据
code = generate_code() # jyn:生成验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:准备重置密码的数据
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
# 提交密码重置请求
# jyn:提交密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向
self.assertEqual(resp.status_code, 302) # jyn:成功重置后通常重定向
# 验证密码是否已更新
# jyn:验证密码是否已更新
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) # 断言用户存在
# 断言密码修改成功
).first() # jyn:type: BlogUser
self.assertNotEqual(blog_user, None) # jyn:断言用户存在
# jyn:断言密码修改成功
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
@ -231,7 +231,7 @@ class AccountTest(TestCase):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com", # 不存在的邮箱
email="123@123.com", # jyn:不存在的邮箱
code="123456",
)
resp = self.client.post(
@ -239,22 +239,22 @@ class AccountTest(TestCase):
data=data
)
self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码
self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
def test_forget_password_email_code_error(self):
"""测试使用错误的验证码重置密码的情况"""
code = generate_code() # 生成正确验证码
utils.set_code(self.blog_user.email, code) # 存储验证码
# 使用错误的验证码
code = generate_code() # jyn:生成正确验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:使用错误的验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # 错误的验证码
code="111111", # jyn:错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码
self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码

@ -1,49 +1,49 @@
from django.urls import path
from django.urls import re_path # 用于正则表达式匹配URL
from django.urls import re_path # jyn:用于正则表达式匹配URL
from . import views # 导入当前应用的视图函数/类
from .forms import LoginForm # 导入自定义的登录表单
from . import views # jyn:导入当前应用的视图函数/类
from .forms import LoginForm # jyn:导入自定义的登录表单
# 定义应用命名空间避免URL名称冲突
# jyn:定义应用命名空间避免URL名称冲突
app_name = "accounts"
# URL路由配置列表映射URL路径到对应的视图
# jyn:URL路由配置列表映射URL路径到对应的视图
urlpatterns = [
# 登录页面路由使用正则匹配以login/结尾的路径
# jyn:登录页面路由使用正则匹配以login/结尾的路径
re_path(r'^login/$',
# 调用LoginView类视图指定登录成功后重定向到首页/
# jyn:调用LoginView类视图指定登录成功后重定向到首页/
views.LoginView.as_view(success_url='/'),
name='login', # URL的名称用于反向解析
# 向视图传递额外参数指定登录表单为自定义的LoginForm
name='login', # jyn:URL的名称用于反向解析
# jyn:向视图传递额外参数指定登录表单为自定义的LoginForm
kwargs={'authentication_form': LoginForm}),
# 注册页面路由匹配以register/结尾的路径
# jyn:注册页面路由匹配以register/结尾的路径
re_path(r'^register/$',
# 调用RegisterView类视图注册成功后重定向到首页
# jyn:调用RegisterView类视图注册成功后重定向到首页
views.RegisterView.as_view(success_url="/"),
name='register'), # URL名称用于反向解析
name='register'), # jyn:URL名称用于反向解析
# 登出功能路由匹配以logout/结尾的路径
# jyn:登出功能路由匹配以logout/结尾的路径
re_path(r'^logout/$',
# 调用LogoutView类视图Django内置或自定义
# jyn:调用LogoutView类视图Django内置或自定义
views.LogoutView.as_view(),
name='logout'), # URL名称
name='logout'), #jyn: URL名称
# 账号操作结果页面路由精确匹配account/result.html路径
# jyn:账号操作结果页面路由精确匹配account/result.html路径
path(r'account/result.html',
# 调用函数视图account_result
# jyn:调用函数视图account_result
views.account_result,
name='result'), # URL名称用于展示注册/验证等结果
name='result'), #jyn: URL名称用于展示注册/验证等结果
# 忘记密码页面路由匹配以forget_password/结尾的路径
# jyn:忘记密码页面路由匹配以forget_password/结尾的路径
re_path(r'^forget_password/$',
# 调用ForgetPasswordView类视图
# jyn:调用ForgetPasswordView类视图
views.ForgetPasswordView.as_view(),
name='forget_password'), # URL名称
name='forget_password'), #jyn: URL名称
# 发送密码重置验证码页面路由匹配以forget_password_code/结尾的路径
# jyn:发送密码重置验证码页面路由匹配以forget_password_code/结尾的路径
re_path(r'^forget_password_code/$',
# 调用ForgetPasswordEmailCode类视图处理发送验证码逻辑
# jyn:调用ForgetPasswordEmailCode类视图处理发送验证码逻辑
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'), # URL名称
name='forget_password_code'), # jyn:URL名称
]

@ -1,5 +1,5 @@
from django.contrib.auth import get_user_model # 获取项目配置的用户模型(支持自定义模型)
from django.contrib.auth.backends import ModelBackend # 导入Django内置的模型认证后端
from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型(支持自定义模型)
from django.contrib.auth.backends import ModelBackend # jyn:导入Django内置的模型认证后端
class EmailOrUsernameModelBackend(ModelBackend):
@ -17,22 +17,22 @@ class EmailOrUsernameModelBackend(ModelBackend):
- password前端传入的密码明文
返回验证成功返回用户对象失败返回None
"""
# 判断输入的「username」是否包含@符号,以此区分邮箱和用户名
# jyn:判断输入的「username」是否包含@符号,以此区分邮箱和用户名
if '@' in username:
# 若包含@,则按邮箱字段查询用户
# jyn:若包含@,则按邮箱字段查询用户
kwargs = {'email': username}
else:
# 若不包含@,则按用户名字段查询用户
# jyn:若不包含@,则按用户名字段查询用户
kwargs = {'username': username}
try:
# 根据上述条件从数据库查询唯一用户
# jyn:根据上述条件从数据库查询唯一用户
user = get_user_model().objects.get(**kwargs)
# 验证密码check_password会自动将明文密码与数据库中存储的哈希密码比对
# jyn:验证密码check_password会自动将明文密码与数据库中存储的哈希密码比对
if user.check_password(password):
return user # 密码正确,返回用户对象(认证成功)
return user # jyn:密码正确,返回用户对象(认证成功)
except get_user_model().DoesNotExist:
# 若查询不到用户(用户名/邮箱不存在返回None认证失败
# jyn:若查询不到用户(用户名/邮箱不存在返回None认证失败
return None
def get_user(self, username):
@ -43,8 +43,8 @@ class EmailOrUsernameModelBackend(ModelBackend):
返回存在则返回用户对象不存在返回None
"""
try:
# 通过主键ID查询用户
# jyn:通过主键ID查询用户
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
# 若用户不存在返回None
# jyn:若用户不存在返回None
return None

@ -1,13 +1,13 @@
import typing
from datetime import timedelta
from django.core.cache import cache # 导入Django缓存模块用于存储验证码
from django.utils.translation import gettext # 用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # 用于延迟翻译文本(支持国际化)
from django.core.cache import cache # jyn:导入Django缓存模块用于存储验证码
from django.utils.translation import gettext # jyn:用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # jyn:用于延迟翻译文本(支持国际化)
from djangoblog.utils import send_email # 导入项目自定义的发送邮件工具函数
from djangoblog.utils import send_email #jyn: 导入项目自定义的发送邮件工具函数
# 验证码有效期5分钟全局变量统一控制时效
# jyn:验证码有效期5分钟全局变量统一控制时效
_code_ttl = timedelta(minutes=5)
@ -19,11 +19,11 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
code: 生成的随机验证码
subject: 邮件主题默认值为Verify Email支持国际化
"""
# 构造邮件HTML内容包含验证码和有效期提示使用%(code)s占位符注入验证码
# jyn:构造邮件HTML内容包含验证码和有效期提示使用%(code)s占位符注入验证码
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
# 调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
# jyn:调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
send_email([to_mail], subject, html_content)
@ -39,11 +39,11 @@ def verify(email: str, code: str) -> typing.Optional[str]:
代码注释中指出当前错误处理逻辑不合理应使用raise抛出异常而非返回错误字符串
若返回错误字符串调用方需额外判断返回值是否为错误增加了代码耦合度
"""
# 从缓存中获取该邮箱对应的验证码
# jyn:从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
# 对比用户输入的验证码与缓存中的验证码
# jyn:对比用户输入的验证码与缓存中的验证码
if cache_code != code:
# 验证码不匹配时,返回国际化的错误提示
#jyn: 验证码不匹配时,返回国际化的错误提示
return gettext("Verification code error")
@ -54,7 +54,7 @@ def set_code(email: str, code: str):
email: 作为缓存key的邮箱地址确保一个邮箱对应一个验证码
code: 需要存入缓存的验证码
"""
# 调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
# jyn:调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
cache.set(email, code, _code_ttl.seconds)
@ -66,5 +66,5 @@ def get_code(email: str) -> typing.Optional[str]:
Return:
缓存中存在该邮箱对应的验证码时返回字符串不存在时返回None
"""
# 调用Django缓存的get方法根据邮箱key获取验证码
# jyn:调用Django缓存的get方法根据邮箱key获取验证码
return cache.get(email)

@ -2,45 +2,45 @@ import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME # 登录后重定向字段名常量
from django.contrib.auth import get_user_model # 获取项目配置的用户模型
from django.contrib.auth import logout # 登出功能函数
from django.contrib.auth.forms import AuthenticationForm # Django内置登录表单
from django.contrib.auth.hashers import make_password # 密码哈希处理函数
from django.http import HttpResponseRedirect, HttpResponseForbidden # HTTP响应类
from django.contrib.auth import REDIRECT_FIELD_NAME # jyn:登录后重定向字段名常量
from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型
from django.contrib.auth import logout # jyn:登出功能函数
from django.contrib.auth.forms import AuthenticationForm # jyn:Django内置登录表单
from django.contrib.auth.hashers import make_password # jyn:密码哈希处理函数
from django.http import HttpResponseRedirect, HttpResponseForbidden # jyn:HTTP响应类
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404, render # 快捷函数
from django.urls import reverse # URL反向解析
from django.utils.decorators import method_decorator # 类视图装饰器工具
from django.utils.http import url_has_allowed_host_and_scheme # 验证重定向URL安全性
from django.views import View # 基础视图类
from django.views.decorators.cache import never_cache # 禁止缓存装饰器
from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器
from django.views.decorators.debug import sensitive_post_parameters # 敏感参数保护装饰器
from django.views.generic import FormView, RedirectView # 通用类视图
from django.shortcuts import get_object_or_404, render # jyn:快捷函数
from django.urls import reverse # jyn:URL反向解析
from django.utils.decorators import method_decorator # jyn:类视图装饰器工具
from django.utils.http import url_has_allowed_host_and_scheme # jyn:验证重定向URL安全性
from django.views import View # jyn:基础视图类
from django.views.decorators.cache import never_cache # jyn:禁止缓存装饰器
from django.views.decorators.csrf import csrf_protect #jyn: CSRF保护装饰器
from django.views.decorators.debug import sensitive_post_parameters # jyn:敏感参数保护装饰器
from django.views.generic import FormView, RedirectView # jyn:通用类视图
from djangoblog.utils import (send_email, get_sha256, get_current_site,
generate_code, delete_sidebar_cache) # 项目工具函数
from . import utils # 当前应用工具函数(验证码相关)
generate_code, delete_sidebar_cache) #jyn: 项目工具函数
from . import utils # jyn:当前应用工具函数(验证码相关)
from .forms import (RegisterForm, LoginForm, ForgetPasswordForm,
ForgetPasswordCodeForm) # 当前应用表单类
from .models import BlogUser # 自定义用户模型
ForgetPasswordCodeForm) # jyn:当前应用表单类
from .models import BlogUser # jyn:自定义用户模型
logger = logging.getLogger(__name__) # 初始化日志记录器
logger = logging.getLogger(__name__) # jyn:初始化日志记录器
# Create your views here.
# jyn:Create your views here.
class RegisterView(FormView):
"""
用户注册类视图继承自FormView处理表单提交的通用视图
负责用户注册表单展示数据验证发送验证邮件及注册结果跳转
"""
form_class = RegisterForm # 指定使用的注册表单
template_name = 'account/registration_form.html' # 注册页面模板路径
form_class = RegisterForm # jyn:指定使用的注册表单
template_name = 'account/registration_form.html' # jyn:注册页面模板路径
@method_decorator(csrf_protect) # 为视图添加CSRF保护
@method_decorator(csrf_protect) # jyn:为视图添加CSRF保护
def dispatch(self, *args, **kwargs):
"""重写分发方法,添加装饰器后调用父类逻辑"""
return super(RegisterView, self).dispatch(*args, **kwargs)
@ -48,26 +48,26 @@ class RegisterView(FormView):
def form_valid(self, form):
"""表单验证通过后执行的逻辑(注册核心流程)"""
if form.is_valid():
# 1. 暂存用户数据不立即保存is_active设为False需邮箱验证后激活
# jyn:1. 暂存用户数据不立即保存is_active设为False需邮箱验证后激活
user = form.save(False)
user.is_active = False # 初始状态:未激活(需邮箱验证)
user.source = 'Register' # 标记注册来源为“前台注册”
user.save(True) # 保存用户到数据库
user.is_active = False # jyn:初始状态:未激活(需邮箱验证)
user.source = 'Register' # jyn:标记注册来源为“前台注册”
user.save(True) # jyn:保存用户到数据库
# 2. 生成邮箱验证链接(包含签名,防止篡改)
site = get_current_site().domain # 获取当前站点域名
# 双重SHA256加密用SECRET_KEY+用户ID生成签名确保链接安全性
# jyn:2. 生成邮箱验证链接(包含签名,防止篡改)
site = get_current_site().domain # jyn:获取当前站点域名
# jyn:双重SHA256加密用SECRET_KEY+用户ID生成签名确保链接安全性
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 开发环境下替换域名(适配本地测试)
# jyn:开发环境下替换域名(适配本地测试)
if settings.DEBUG:
site = '127.0.0.1:8000'
# 反向解析结果页URL拼接完整验证链接
# jyn:反向解析结果页URL拼接完整验证链接
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 3. 构造验证邮件内容并发送
# jyn:3. 构造验证邮件内容并发送
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -79,16 +79,16 @@ class RegisterView(FormView):
{url}
""".format(url=url)
send_email(
emailto=[user.email], # 收件人邮箱(新注册用户的邮箱)
title='验证您的电子邮箱', # 邮件标题
content=content # 邮件HTML内容
emailto=[user.email], # jyn:收件人邮箱(新注册用户的邮箱)
title='验证您的电子邮箱', #jyn: 邮件标题
content=content #jyn: 邮件HTML内容
)
# 4. 跳转到注册结果页(提示用户查收验证邮件)
# jyn:4. 跳转到注册结果页(提示用户查收验证邮件)
url = reverse('accounts:result') + f'?type=register&id={str(user.id)}'
return HttpResponseRedirect(url)
else:
# 表单验证失败,重新渲染表单并显示错误
# jyn:表单验证失败,重新渲染表单并显示错误
return self.render_to_response({'form': form})
@ -97,18 +97,18 @@ class LogoutView(RedirectView):
用户登出类视图继承自RedirectView处理重定向的通用视图
负责清除用户会话缓存并重定向到登录页
"""
url = '/login/' # 登出后默认重定向地址(登录页)
url = '/login/' # jyn:登出后默认重定向地址(登录页)
@method_decorator(never_cache) # 禁止缓存登出页面,避免浏览器缓存导致的问题
@method_decorator(never_cache) # jyn:禁止缓存登出页面,避免浏览器缓存导致的问题
def dispatch(self, request, *args, **kwargs):
"""重写分发方法,添加装饰器后调用父类逻辑"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""处理GET请求登出核心逻辑"""
logout(request) # 清除用户会话,实现登出
delete_sidebar_cache() # 删除侧边栏缓存(可能存储了用户相关信息)
return super(LogoutView, self).get(request, *args, **kwargs) # 执行重定向
logout(request) # jyn:清除用户会话,实现登出
delete_sidebar_cache() # jyn:删除侧边栏缓存(可能存储了用户相关信息)
return super(LogoutView, self).get(request, *args, **kwargs) # jyn:执行重定向
class LoginView(FormView):
@ -116,14 +116,14 @@ class LoginView(FormView):
用户登录类视图继承自FormView
负责登录表单展示数据验证用户认证记住登录状态及重定向
"""
form_class = LoginForm # 指定使用的自定义登录表单
template_name = 'account/login.html' # 登录页面模板路径
success_url = '/' # 登录成功默认重定向地址(首页)
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名默认next
login_ttl = 2626560 # “记住登录”状态的有效期约等于1个月
# 为视图添加多重装饰器敏感参数保护、CSRF保护、禁止缓存
@method_decorator(sensitive_post_parameters('password')) # 保护密码参数,避免日志泄露
form_class = LoginForm #jyn: 指定使用的自定义登录表单
template_name = 'account/login.html' # jyn:登录页面模板路径
success_url = '/' # jyn:登录成功默认重定向地址(首页)
redirect_field_name = REDIRECT_FIELD_NAME # jyn:重定向字段名默认next
login_ttl = 2626560 #jyn: “记住登录”状态的有效期约等于1个月
# jyn:为视图添加多重装饰器敏感参数保护、CSRF保护、禁止缓存
@method_decorator(sensitive_post_parameters('password')) # jyn:保护密码参数,避免日志泄露
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
@ -131,41 +131,41 @@ class LoginView(FormView):
def get_context_data(self, **kwargs):
"""添加额外上下文数据(重定向地址)到模板"""
# 获取URL中的重定向参数如登录前访问的受保护页面
# jyn:获取URL中的重定向参数如登录前访问的受保护页面
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/' # 默认重定向到首页
kwargs['redirect_to'] = redirect_to # 将重定向地址传入模板上下文
redirect_to = '/' # jyn:默认重定向到首页
kwargs['redirect_to'] = redirect_to # jyn:将重定向地址传入模板上下文
return super(LoginView, self).get_context_data(** kwargs)
def form_valid(self, form):
"""表单验证通过后执行的逻辑(登录核心流程)"""
# 用Django内置AuthenticationForm重新验证确保认证逻辑符合默认规范
# jyn:用Django内置AuthenticationForm重新验证确保认证逻辑符合默认规范
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache() # 删除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # 日志记录重定向字段名
delete_sidebar_cache() # jyn:删除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # jyn:日志记录重定向字段名
# 执行登录:将用户信息存入会话
# jyn:执行登录:将用户信息存入会话
auth.login(self.request, form.get_user())
# 处理“记住我”功能若勾选设置会话有效期为1个月
# jyn:处理“记住我”功能若勾选设置会话有效期为1个月
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
# 调用父类form_valid执行重定向
# jyn:调用父类form_valid执行重定向
return super(LoginView, self).form_valid(form)
else:
# 表单验证失败(如密码错误),重新渲染表单并显示错误
# jyn:表单验证失败(如密码错误),重新渲染表单并显示错误
return self.render_to_response({'form': form})
def get_success_url(self):
"""自定义登录成功后的重定向地址优先使用URL中的next参数"""
# 获取POST请求中的重定向地址用户登录前尝试访问的页面
# jyn:获取POST请求中的重定向地址用户登录前尝试访问的页面
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证重定向地址的安全性:避免跳转到外部恶意网站
# jyn:验证重定向地址的安全性:避免跳转到外部恶意网站
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]):
redirect_to = self.success_url # 不安全则使用默认重定向地址
redirect_to = self.success_url # jyn:不安全则使用默认重定向地址
return redirect_to
@ -174,48 +174,48 @@ def account_result(request):
账号操作结果视图函数函数视图
处理注册成功提示邮箱验证逻辑并展示结果页面
"""
# 从URL参数中获取操作类型register/validation和用户ID
# jyn:从URL参数中获取操作类型register/validation和用户ID
type = request.GET.get('type')
id = request.GET.get('id')
# 获取对应的用户若不存在则返回404
# jyn:获取对应的用户若不存在则返回404
user = get_object_or_404(get_user_model(), id=id)
logger.info(type) # 日志记录操作类型
# 若用户已激活,直接重定向到首页(避免重复验证)
# jyn:若用户已激活,直接重定向到首页(避免重复验证)
if user.is_active:
return HttpResponseRedirect('/')
# 处理合法的操作类型(注册成功提示/邮箱验证)
# jyn:处理合法的操作类型(注册成功提示/邮箱验证)
if type and type in ['register', 'validation']:
if type == 'register':
# 注册成功:提示用户查收验证邮件
# jyn:注册成功:提示用户查收验证邮件
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证:验证签名是否正确,正确则激活用户
# 重新计算签名与URL中的签名对比防止链接篡改
# jyn:邮箱验证:验证签名是否正确,正确则激活用户
# jyn:重新计算签名与URL中的签名对比防止链接篡改
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden() # 签名不匹配返回403禁止访问
# 激活用户将is_active设为True
return HttpResponseForbidden() # jyn:签名不匹配返回403禁止访问
# jyn:激活用户将is_active设为True
user.is_active = True
user.save()
# 验证成功:提示用户可登录
# jyn:验证成功:提示用户可登录
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面,传递标题和内容
# jyn:渲染结果页面,传递标题和内容
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
# 操作类型不合法,重定向到首页
# jyn:操作类型不合法,重定向到首页
return HttpResponseRedirect('/')
@ -224,23 +224,23 @@ class ForgetPasswordView(FormView):
忘记密码类视图继承自FormView
负责密码重置表单展示数据验证及更新用户密码
"""
form_class = ForgetPasswordForm # 指定使用的密码重置表单
template_name = 'account/forget_password.html' # 密码重置页面模板路径
form_class = ForgetPasswordForm # jyn:指定使用的密码重置表单
template_name = 'account/forget_password.html' # jyn:密码重置页面模板路径
def form_valid(self, form):
"""表单验证通过后执行的逻辑(密码重置核心流程)"""
if form.is_valid():
# 1. 获取表单中的邮箱,查询对应的用户
# jyn:1. 获取表单中的邮箱,查询对应的用户
blog_user = BlogUser.objects.filter(
email=form.cleaned_data.get("email")
).get()
# 2. 对新密码进行哈希处理,并更新用户密码
# jyn:2. 对新密码进行哈希处理,并更新用户密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
# 3. 密码重置成功,重定向到登录页
# jyn:3. 密码重置成功,重定向到登录页
return HttpResponseRedirect('/login/')
else:
# 表单验证失败(如验证码错误、密码不一致),重新渲染表单
# jyn:表单验证失败(如验证码错误、密码不一致),重新渲染表单
return self.render_to_response({'form': form})
@ -251,16 +251,16 @@ class ForgetPasswordEmailCode(View):
"""
def post(self, request: HttpRequest):
"""处理POST请求发送验证码核心逻辑"""
# 1. 验证邮箱表单数据
#jyn: 1. 验证邮箱表单数据
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") # 邮箱格式不合法,返回错误提示
return HttpResponse("错误的邮箱") # jyn:邮箱格式不合法,返回错误提示
# 2. 生成验证码并发送邮件
to_email = form.cleaned_data["email"] # 获取合法的邮箱地址
code = generate_code() # 生成随机验证码
utils.send_verify_email(to_email, code) # 发送验证码邮件
utils.set_code(to_email, code) # 将验证码存入缓存(设置有效期)
# jyn:2. 生成验证码并发送邮件
to_email = form.cleaned_data["email"] # jyn:获取合法的邮箱地址
code = generate_code() # jyn:生成随机验证码
utils.send_verify_email(to_email, code) # jyn:发送验证码邮件
utils.set_code(to_email, code) # jyn:将验证码存入缓存(设置有效期)
# 3. 操作成功返回“ok”提示
# jyn:3. 操作成功返回“ok”提示
return HttpResponse("ok")

@ -1,47 +1,83 @@
# jyn: 该模块用于配置 Django 管理后台的评论Comment模型管理界面
# jyn: 功能包括:自定义评论启用/禁用批量操作、列表页展示字段配置、关联对象跳转链接、界面显示优化等
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 _
from django.urls import reverse # jyn: 用于反向解析 admin 后台的模型编辑页面 URL
from django.utils.html import format_html # jyn: 用于安全生成 HTML 标签,避免 XSS 风险
from django.utils.translation import gettext_lazy as _ # jyn: 用于国际化翻译,支持多语言显示
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
"""
jyn: 批量禁用评论的自定义操作函数
:param modeladmin: 关联的 ModelAdmin 实例
:param request: 当前请求对象
:param queryset: 选中的评论记录集合
"""
queryset.update(is_enable=False) # jyn: 将选中评论的 is_enable 字段设为 False实现禁用
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
"""
jyn: 批量启用评论的自定义操作函数
:param modeladmin: 关联的 ModelAdmin 实例
:param request: 当前请求对象
:param queryset: 选中的评论记录集合
"""
queryset.update(is_enable=True) # jyn: 将选中评论的 is_enable 字段设为 True实现启用
# jyn: 为批量操作函数设置后台显示名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20
"""jyn: 评论模型Comment的 Admin 配置类,控制后台展示和操作逻辑"""
list_per_page = 20 # jyn: 列表页每页显示 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]
'id', # jyn: 评论 ID
'body', # jyn: 评论内容
'link_to_userinfo', # jyn: 自定义字段,显示评论作者跳转链接
'link_to_article', # jyn: 自定义字段,显示关联文章跳转链接
'is_enable', # jyn: 评论启用状态True/False
'creation_time' # jyn: 评论创建时间
)
list_display_links = ('id', 'body', 'is_enable') # jyn: 列表页中可点击跳转编辑页的字段
list_filter = ('is_enable',) # jyn: 右侧筛选器,按启用状态筛选评论
exclude = ('creation_time', 'last_modify_time') # jyn: 编辑页隐藏的字段(自动生成,无需手动修改)
actions = [disable_commentstatus, enable_commentstatus] # jyn: 注册批量操作函数
def link_to_userinfo(self, obj):
"""
jyn: 自定义列表字段生成评论作者的 admin 编辑页跳转链接
:param obj: 当前 Comment 模型实例
:return: 包含跳转链接的 HTML 字符串
"""
# jyn: 获取作者模型UserInfo的 app 名称和模型名称,用于反向解析 URL
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# jyn: 反向解析作者模型的编辑页 URL传入作者 ID 作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# jyn: 生成 HTML 链接,优先显示昵称,无昵称则显示邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
(link, obj.author.nickname if obj.author.nickname else obj.author.email)
)
def link_to_article(self, obj):
"""
jyn: 自定义列表字段生成关联文章的 admin 编辑页跳转链接
:param obj: 当前 Comment 模型实例
:return: 包含跳转链接的 HTML 字符串
"""
# jyn: 获取文章模型Article的 app 名称和模型名称
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# jyn: 反向解析文章模型的编辑页 URL传入文章 ID 作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# jyn: 生成 HTML 链接,显示文章标题
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
u'<a href="%s">%s</a>' % (link, obj.article.title)
)
# jyn: 为自定义列表字段设置后台显示名称(支持国际化)
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,5 +1,8 @@
from django.apps import AppConfig
# jyn: 评论应用comments的配置类用于定义应用元数据和初始化行为
from django.apps import AppConfig # jyn: Django 内置的应用配置基类,所有应用配置需继承此类
class CommentsConfig(AppConfig):
name = 'comments'
"""jyn: comments 应用的核心配置类,指定应用名称等关键信息"""
name = 'comments' # jyn: 应用唯一标识名称与项目目录名一致Django 通过该名称识别应用

@ -1,13 +1,15 @@
# jyn: 评论功能的表单配置模块,定义前端提交评论的表单结构和字段规则
from django import forms
from django.forms import ModelForm
from .models import Comment
from django.forms import ModelForm # jyn: Django 内置模型表单基类,自动关联模型字段生成表单
from .models import Comment # jyn: 导入评论核心模型,表单与模型数据联动
class CommentForm(ModelForm):
"""jyn: 评论提交表单类,继承 ModelForm 实现模型与表单的自动映射"""
# jyn: 父评论 ID 字段,隐藏输入(用于回复功能),非必填(首次评论无父评论)
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
model = Comment
fields = ['body']
model = Comment # jyn: 关联的核心模型,表单数据同步至该模型
fields = ['body'] # jyn: 前端需提交的字段(评论内容),自动生成对应表单控件

@ -1,39 +1,54 @@
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
# jyn: 评论功能核心数据模型模块,定义评论的数据库结构、关联关系及基础配置
from django.conf import settings # jyn: 导入 Django 项目配置,用于获取自定义用户模型
from django.db import models # jyn: Django 数据库模型基类,所有数据模型需继承此类
from django.utils.timezone import now # jyn: 获取当前时区时间,用于时间字段默认值
from django.utils.translation import gettext_lazy as _ # jyn: 国际化翻译支持
from blog.models import Article # jyn: 导入博客文章模型,建立评论与文章的关联
from blog.models import Article
# Create your models here.
class Comment(models.Model):
"""jyn: 评论数据模型,存储评论核心信息,关联用户、文章及父评论(支持回复功能)"""
# jyn: 评论正文,文本字段,最大长度限制 300 字符
body = models.TextField('正文', max_length=300)
# jyn: 评论创建时间,默认值为当前时间,支持国际化显示
creation_time = models.DateTimeField(_('creation time'), default=now)
# jyn: 评论最后修改时间,默认值为当前时间,支持国际化显示
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# jyn: 关联评论作者(用户模型),级联删除(用户删除时评论同步删除),支持国际化
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
on_delete=models.CASCADE
)
# jyn: 关联所属文章,级联删除(文章删除时评论同步删除),支持国际化
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
on_delete=models.CASCADE
)
# jyn: 关联父评论(自关联),支持回复功能,允许为空,级联删除,支持国际化
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)
on_delete=models.CASCADE
)
# jyn: 评论启用状态,布尔值,默认禁用(需后台审核),非空约束
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'
"""jyn: 模型元数据配置,控制数据库表结构和 Admin 界面行为"""
ordering = ['-id'] # jyn: 数据查询默认排序:按评论 ID 倒序(最新评论在前)
verbose_name = _('comment') # jyn: 模型单数显示名称(支持国际化)
verbose_name_plural = verbose_name # jyn: 模型复数显示名称(与单数一致)
get_latest_by = 'id' # jyn: 使用 latest() 方法时,按 ID 字段获取最新记录
def __str__(self):
"""jyn: 模型实例的字符串表示Admin 界面及打印时显示评论正文"""
return self.body

@ -1,38 +1,50 @@
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
# jyn: 评论功能测试模块,涵盖评论提交、审核状态、回复功能、模板标签及工具函数的完整性测试
from django.test import Client, RequestFactory, TransactionTestCase # jyn: Django 测试核心类Client模拟请求RequestFactory构造请求TransactionTestCase支持事务回滚
from django.urls import reverse # jyn: 反向解析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
from accounts.models import BlogUser # jyn: 导入用户模型,用于创建测试用户
from blog.models import Category, Article # jyn: 导入博客分类、文章模型,用于创建测试文章
from comments.models import Comment # jyn: 导入评论模型,用于验证评论数据
from comments.templatetags.comments_tags import * # jyn: 导入评论相关模板标签,测试模板渲染逻辑
from djangoblog.utils import get_max_articleid_commentid # jyn: 导入工具函数测试ID获取功能
# Create your tests here.
class CommentsTest(TransactionTestCase):
"""jyn: 评论功能集成测试类,覆盖评论提交、状态更新、回复、模板标签及邮件通知等核心流程"""
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
"""jyn: 测试前置初始化方法,每次测试用例执行前自动调用,创建测试依赖数据"""
self.client = Client() # jyn: 初始化测试客户端用于模拟前端HTTP请求
self.factory = RequestFactory() # jyn: 初始化请求工厂,用于构造自定义请求对象
# jyn: 创建博客设置,开启评论需审核功能(模拟真实环境的评论审核机制)
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.comment_need_review = True # jyn: 评论需后台审核才能显示
value.save()
# jyn: 创建超级用户,用于测试登录状态下的评论提交
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()
"""
jyn: 辅助方法批量更新指定文章下所有评论的启用状态模拟后台审核通过
:param article: 目标文章实例
"""
comments = article.comment_set.all() # jyn: 获取文章关联的所有评论
for comment in comments:
comment.is_enable = True
comment.save()
comment.is_enable = True # jyn: 设为启用状态
comment.save() # jyn: 保存修改
def test_validate_comment(self):
"""jyn: 核心测试用例,验证评论提交、审核、回复、模板标签及工具函数的正确性"""
# 1. 模拟用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 2. 创建测试分类和文章(评论的关联对象)
category = Category()
category.name = "categoryccc"
category.save()
@ -42,39 +54,41 @@ class CommentsTest(TransactionTestCase):
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' # jyn: 文章类型(假设'a'代表普通文章)
article.status = 'p' # jyn: 文章状态(假设'p'代表已发布)
article.save()
# 3. 生成评论提交接口的URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
'article_id': article.id}) # jyn: 反向解析"提交评论"接口传入文章ID
# 4. 测试首次提交普通评论(未审核状态)
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
'body': '123ffffffffff' # jyn: 评论正文
})
self.assertEqual(response.status_code, 302) # jyn: 验证提交成功后重定向状态码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()), 0) # jyn: 假设comment_list()返回启用的评论
self.assertEqual(len(article.comment_list()), 1)
# 模拟审核通过,再次验证评论数量
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1) # jyn: 审核后应显示1条评论
# 5. 测试再次提交相同内容评论(验证重复提交允许性)
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.assertEqual(response.status_code, 302) # jyn: 验证提交成功
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
parent_comment_id = article.comment_list()[0].id
self.assertEqual(len(article.comment_list()), 2) # jyn: 评论总数应为2条
# 6. 测试回复功能提交带父评论ID的评论
parent_comment_id = article.comment_list()[0].id # jyn: 获取第一条评论ID作为父评论
response = self.client.post(comment_url,
{
'body': '''
@ -87,23 +101,24 @@ class CommentsTest(TransactionTestCase):
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
''', # jyn: 带Markdown格式、链接、代码块的评论正文
'parent_comment_id': parent_comment_id # jyn: 指定父评论ID实现回复
})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.status_code, 302) # jyn: 验证回复提交成功
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)
self.assertEqual(len(article.comment_list()), 3) # jyn: 评论总数应为3条含1条回复
# 7. 测试评论树解析(验证回复层级关系)
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment) # jyn: 调用模板标签解析评论树
self.assertEqual(len(tree), 1) # jyn: 验证父评论下有1条子回复
# 8. 测试评论项渲染模板标签(验证模板标签功能正常)
data = show_comment_item(comment, True) # jyn: 调用模板标签生成评论HTML片段
self.assertIsNotNone(data) # jyn: 验证渲染结果非空
# 9. 测试工具函数验证最大ID获取功能
s = get_max_articleid_commentid() # jyn: 调用工具函数获取最大文章ID和评论ID
self.assertIsNotNone(s) # jyn: 验证返回结果非空
# 10. 测试评论邮件通知(验证邮件发送功能)
from comments.utils import send_comment_email
send_comment_email(comment)
send_comment_email(comment) # jyn: 调用工具函数发送评论通知邮件(无返回值,验证无报错)

@ -1,11 +1,14 @@
from django.urls import path
# jyn: 评论应用comments的URL配置模块定义评论相关接口的路由映射
from django.urls import path # jyn: Django URL路径匹配核心函数用于定义路由规则
from . import views # jyn: 导入当前应用的视图模块,关联路由与视图逻辑
from . import views
app_name = "comments" # jyn: 路由命名空间,避免不同应用间路由名称冲突
app_name = "comments"
urlpatterns = [
# jyn: 提交评论接口路由接收文章ID参数映射到CommentPostView视图类
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
'article/<<int:article_id>/postcomment', # jyn: 路由路径,<<int:article_id>接收整数型文章ID参数
views.CommentPostView.as_view(), # jyn: 将请求分发到基于类的视图CommentPostView需调用as_view()转换)
name='postcomment' # jyn: 路由名称用于reverse反向解析URL
),
]

@ -1,28 +1,46 @@
import logging
# jyn: 评论功能邮件通知工具模块,用于评论提交后向评论者及被回复者发送邮件通知
import logging # jyn: 日志模块,记录邮件发送过程中的异常信息
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _ # jyn: 国际化翻译支持,邮件内容适配多语言
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
from djangoblog.utils import get_current_site # jyn: 工具函数,获取当前网站域名
from djangoblog.utils import send_email # jyn: 工具函数,封装邮件发送逻辑
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # jyn: 创建日志实例,用于记录模块内的日志信息
def send_comment_email(comment):
"""
jyn: 发送评论相关邮件通知的核心函数
:param comment: 已保存的 Comment 模型实例新提交的评论或回复
:return: 无返回值内部处理邮件发送逻辑及异常捕获
"""
# jyn: 获取当前网站域名,用于拼接文章访问链接
site = get_current_site().domain
# jyn: 邮件主题(支持国际化)
subject = _('Thanks for your comment')
# jyn: 拼接文章的完整访问链接HTTPS协议
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 1. 向当前评论者发送「评论提交成功」通知邮件
# jyn: 构建评论者邮件的HTML内容包含文章链接和感谢语支持国际化
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)
%(article_url)s""") % {
'article_url': article_url,
'article_title': comment.article.title
}
tomail = comment.author.email # jyn: 获取当前评论者的邮箱地址
send_email([tomail], subject, html_content) # jyn: 调用工具函数发送邮件
# 2. 若当前评论是回复(有父评论),向父评论者发送「收到回复」通知邮件
try:
if comment.parent_comment:
if comment.parent_comment: # jyn: 判断当前评论是否为对其他评论的回复
# jyn: 构建父评论者邮件的HTML内容告知其评论被回复支持国际化
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
<br/>
@ -30,9 +48,12 @@ def send_comment_email(comment):
<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)
""") % {
'article_url': article_url,
'article_title': comment.article.title,
'comment_body': comment.parent_comment.body # jyn: 包含父评论的正文内容
}
tomail = comment.parent_comment.author.email # jyn: 获取父评论者的邮箱地址
send_email([tomail], subject, html_content) # jyn: 发送回复通知邮件
except Exception as e:
logger.error(e)
logger.error(e) # jyn: 捕获发送过程中的异常,记录错误日志(不中断程序执行)

@ -1,63 +1,76 @@
# 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
# jyn: 评论功能核心视图模块,处理评论提交、数据验证、权限判断及页面跳转逻辑
from django.core.exceptions import ValidationError # jyn: Django 数据验证异常类,用于抛出评论相关错误
from django.http import HttpResponseRedirect # jyn: 重定向响应类,评论提交后跳转至指定页面
from django.shortcuts import get_object_or_404 # jyn: 快捷查询函数查询不到数据时返回404
from django.utils.decorators import method_decorator # jyn: 方法装饰器工具,用于给类视图方法添加装饰器
from django.views.decorators.csrf import csrf_protect # jyn: CSRF防护装饰器防止跨站请求伪造
from django.views.generic.edit import FormView # jyn: 基于类的表单处理视图基类,简化表单验证逻辑
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
from accounts.models import BlogUser # jyn: 导入用户模型,关联评论作者
from blog.models import Article # jyn: 导入文章模型,关联评论所属文章
from .forms import CommentForm # jyn: 导入评论表单类,用于前端数据验证
from .models import Comment # jyn: 导入评论模型,用于数据存储
class CommentPostView(FormView):
form_class = CommentForm
template_name = 'blog/article_detail.html'
"""jyn: 评论提交处理视图类继承FormView实现表单验证、数据保存及页面跳转"""
form_class = CommentForm # jyn: 指定关联的表单类,用于前端提交数据的验证
template_name = 'blog/article_detail.html' # jyn: 表单验证失败时渲染的模板(文章详情页)
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""jyn: 重写dispatch方法添加CSRF防护所有请求先经过该方法分发"""
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
"""jyn: 处理GET请求直接重定向到文章详情页的评论区"""
article_id = self.kwargs['article_id'] # jyn: 从URL参数中获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询文章不存在则返回404
url = article.get_absolute_url() # jyn: 获取文章的绝对URL
return HttpResponseRedirect(url + "#comments") # jyn: 重定向到文章评论区锚点
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
"""jyn: 表单数据验证失败时的处理逻辑,返回文章详情页并携带错误表单"""
article_id = self.kwargs['article_id'] # jyn: 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询目标文章
# jyn: 渲染文章详情页,传递错误表单和文章实例(前端显示错误信息)
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)
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
"""jyn: 表单数据验证合法后的核心逻辑,保存评论数据并跳转"""
user = self.request.user # jyn: 获取当前登录用户
author = BlogUser.objects.get(pk=user.pk) # jyn: 通过用户ID查询BlogUser实例评论作者
article_id = self.kwargs['article_id'] # jyn: 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询目标文章不存在返回404
# jyn: 校验文章评论状态和发布状态,禁止对关闭评论/草稿文章提交评论
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
raise ValidationError("该文章评论已关闭.") # jyn: 抛出验证错误,终止评论提交
# jyn: 不立即保存评论commit=False先补充关联字段
comment = form.save(False)
comment.article = article
comment.article = article # jyn: 关联评论所属文章
# jyn: 根据博客设置决定评论是否需要审核(无需审核则直接启用)
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
settings = get_blog_setting() # jyn: 获取博客全局设置
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
comment.is_enable = True # jyn: 无需审核时,评论直接启用
comment.author = author # jyn: 关联评论作者
# jyn: 处理回复功能若存在父评论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
pk=form.cleaned_data['parent_comment_id']) # jyn: 查询父评论
comment.parent_comment = parent_comment # jyn: 关联父评论
comment.save(True) # jyn: 最终保存评论数据到数据库
comment.save(True)
# jyn: 重定向到文章详情页的当前评论锚点(精准定位到新提交的评论)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

Loading…
Cancel
Save