Compare commits

..

59 Commits

Author SHA1 Message Date
pu685px73 b84a4636b8 ADD file via upload
4 months ago
pu685px73 cc72205f7b ADD file via upload
4 months ago
pu685px73 59198b6ef4 ADD file via upload
4 months ago
pu685px73 14ae1ee140 ADD file via upload
4 months ago
pu685px73 aa3da56be8 ADD file via upload
4 months ago
pu685px73 94f9530594 ADD file via upload
4 months ago
pu685px73 302503775c ADD file via upload
4 months ago
pu685px73 ed4ec24e39 ADD file via upload
4 months ago
pu685px73 ad6a8da984 ADD file via upload
4 months ago
pu685px73 dd7abc979c ADD file via upload
4 months ago
pu685px73 ddd8b6ca42 ADD file via upload
4 months ago
pu685px73 1306cc26df ADD file via upload
4 months ago
pu685px73 7f58515a0d ADD file via upload
4 months ago
pu685px73 9c1b640398 ADD file via upload
4 months ago
pu685px73 1313ac2702 ADD file via upload
4 months ago
pu685px73 ef41edfb9f ADD file via upload
4 months ago
pu685px73 f707a5cfb2 ADD file via upload
4 months ago
pu685px73 bbe481bd73 ADD file via upload
4 months ago
pu685px73 19e3e20724 ADD file via upload
4 months ago
pu685px73 e3d1462ab5 ADD file via upload
4 months ago
pu685px73 ae7972a233 ADD file via upload
4 months ago
pu685px73 343b352824 ADD file via upload
4 months ago
pu685px73 0a71263a98 ADD file via upload
4 months ago
pu685px73 892e93c728 ADD file via upload
4 months ago
pu685px73 8788617476 ADD file via upload
4 months ago
pu685px73 63c669b1e8 ADD file via upload
4 months ago
pu685px73 ca64e1e03e ADD file via upload
4 months ago
pu685px73 e7fe4f236a ADD file via upload
4 months ago
pu685px73 c1da8d414f ADD file via upload
4 months ago
pu685px73 ff4ae7672f Delete 'src/comments/__pycache__/ admin.cpython-312.pyc'
4 months ago
pu685px73 5d794c150b ADD file via upload
4 months ago
pu685px73 b8e3bc6fc0 Delete 'src/comments/__pycache__/ __init__.cpython-312.pyc'
4 months ago
pu685px73 1dc56841bc ADD file via upload
4 months ago
pu685px73 f05deb4f4b ADD file via upload
4 months ago
pu685px73 cbb33c4ddc ADD file via upload
4 months ago
pu685px73 778adb3c98 Delete 'comment'
4 months ago
pu685px73 f633cd6dc7 ADD file via upload
4 months ago
pu685px73 3c05c696ff Delete 'src/comments'
4 months ago
pu685px73 a87f385f6e 1
4 months ago
pu685px73 0a380a6067 Delete 'src/__pycache__'
4 months ago
pu685px73 775e437dd3 Add __pycache__
4 months ago
pu685px73 2853092d52 Delete 'src/__pycache__'
4 months ago
pu685px73 4f326fcbcc ADD file via upload
4 months ago
pu685px73 bd325ddb00 Delete 's'
4 months ago
pu685px73 12316252a7 ADD file via upload
4 months ago
pu685px73 4856f61de3 Delete 'src/1'
4 months ago
pu685px73 37c3621f54 Delete 'src/bb'
4 months ago
pu685px73 f91f89b055 Add bb
4 months ago
pu685px73 c03d77d5ce Delete 'ddd'
4 months ago
pu685px73 7dad061f41 Delete 'bb'
4 months ago
pu685px73 dd131d8217 ADD file via upload
4 months ago
pu685px73 4c20cda373 ADD file via upload
4 months ago
pu685px73 f52ab27273 Add 1
4 months ago
pu685px73 c512dc8c63 ADD file via upload
4 months ago
pu685px73 54a46fb553 ADD file via upload
4 months ago
pu685px73 6257f61e35 Merge pull request '2' (#6) from zjy_branch into develop
5 months ago
pjf284zq7 bed7097354 Merge pull request '1' (#5) from lm_branch into develop
5 months ago
pu685px73 547d184482 Update README.md
5 months ago
pu685px73 971c23ee3b Update README.md
5 months ago

@ -1,2 +1 @@
基于Python的Hello World
基于Python的Hello World

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,68 +0,0 @@
#lm 用户数据模型模块
#lm 功能定义自定义用户模型BlogUser扩展Django内置用户模型功能
#lm 包含用户基本信息字段和相关的业务方法
# 导入Django内置的抽象用户模型用于扩展自定义用户功能包含基础用户名、密码等字段
from django.contrib.auth.models import AbstractUser
# 导入Django模型相关类用于定义数据库表结构
from django.db import models
# 导入reverse函数用于通过URL名称生成对应的URL路径
from django.urls import reverse
# 导入now函数用于获取当前时区的时间作为字段默认值
from django.utils.timezone import now
# 导入翻译工具,用于实现模型字段名称的国际化
from django.utils.translation import gettext_lazy as _
# 导入自定义工具函数get_current_site用于获取当前站点的域名信息
from djangoblog.utils import get_current_site
class BlogUser(AbstractUser):
"""
自定义用户模型继承自Django的AbstractUser
扩展了内置用户模型增加了昵称创建时间修改时间创建来源等自定义字段
"""
#lm 昵称字段支持国际化标签最大长度100允许空白不强制填写
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
#lm 创建时间字段支持国际化标签默认值为当前时间调用now函数
creation_time = models.DateTimeField(_('creation time'), default=now)
#lm 最后修改时间字段:支持国际化标签,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#lm 创建来源字段记录用户创建的渠道如adminsite、frontend等支持国际化标签允许空白
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""
定义模型实例的绝对URL标准Django方法
通过URL名称'blog:author_detail'生成用户详情页的URL参数为用户名
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username}) #lm kwargs传递URL所需的用户名参数
def __str__(self):
"""
定义模型实例的字符串表示
当打印或引用用户实例时返回用户的邮箱地址便于识别
"""
return self.email
def get_full_url(self):
"""
生成用户详情页的完整URL包含站点域名
结合当前站点域名和get_absolute_url生成的相对路径组成完整链接
"""
#lm 获取当前站点的域名如www.example.com
site = get_current_site().domain
#lm 拼接域名和相对路径形成完整URL使用HTTPS协议
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
"""
模型的元数据配置用于定义模型的显示和行为规则
"""
ordering = ['-id'] #lm 数据查询时的默认排序按ID降序最新创建的用户在前
verbose_name = _('user') #lm 模型的单数显示名称(支持国际化)
verbose_name_plural = verbose_name #lm 模型的复数显示名称(与单数一致,避免英文复数变形问题)
get_latest_by = 'id' #lm 获取"最新记录"时的依据字段按ID判断ID最大的为最新

@ -1 +0,0 @@
undefined

@ -1,91 +0,0 @@
#lm 用户管理后台配置模块
#lm 功能定义Django admin后台中用户管理的自定义表单和管理员配置
#lm 包含用户创建表单、用户修改表单和用户管理员类的自定义实现
# 导入Django表单基类
from django import forms
# 导入Django内置的用户管理员类用于扩展自定义用户的admin配置
from django.contrib.auth.admin import UserAdmin
# 导入Django内置的用户修改表单用于扩展自定义用户的修改表单
from django.contrib.auth.forms import UserChangeForm
# 导入Django内置的用户名字段类用于自定义用户名字段验证
from django.contrib.auth.forms import UsernameField
# 导入翻译工具,用于实现字段名称的国际化
from django.utils.translation import gettext_lazy as _
# 注册模型时需要导入自定义的用户模型
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""自定义用户创建表单用于在admin站点添加新用户时使用"""
#lm 密码字段,使用密码输入框(输入内容隐藏),标签支持国际化
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
#lm 确认密码字段,用于验证两次输入的密码是否一致
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
#lm 关联的模型为自定义的BlogUser
model = BlogUser
#lm 创建用户时需要填写的字段这里仅指定email其他字段可默认或后续补充
fields = ('email',)
def clean_password2(self):
"""验证两次输入的密码是否一致"""
#lm 获取清洗后的密码1和密码2
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
#lm 如果两次密码都存在且不一致,抛出验证错误
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
"""保存用户,将密码加密处理,并设置创建来源"""
#lm 调用父类的save方法先不提交到数据库commit=False
user = super().save(commit=False)
#lm 对密码进行加密处理Django内置的密码哈希方法
user.set_password(self.cleaned_data["password1"])
#lm 如果需要提交到数据库
if commit:
#lm 设置用户的创建来源为'adminsite'表示通过admin站点创建
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
"""自定义用户修改表单用于在admin站点编辑用户信息时使用"""
class Meta:
#lm 关联的模型为BlogUser
model = BlogUser
#lm 显示所有字段(可根据需要指定具体字段)
fields = '__all__'
#lm 指定用户名字段的处理类为UsernameField提供内置验证
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
"""初始化方法,调用父类的初始化逻辑"""
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""自定义用户管理员类用于在admin站点配置BlogUser的展示和操作"""
#lm 指定修改用户时使用的表单
form = BlogUserChangeForm
#lm 指定添加用户时使用的表单
add_form = BlogUserCreationForm
#lm 列表页展示的字段
list_display = (
'id', # 用户ID
'nickname', # 昵称
'username', # 用户名
'email', # 邮箱
'last_login', # 最后登录时间
'date_joined', # 注册时间
'source' # 创建来源
)
#lm 列表页中可点击跳转详情页的字段
list_display_links = ('id', 'username')
#lm 列表页的排序方式按ID降序即最新创建的用户在前
ordering = ('-id',)

@ -1,15 +0,0 @@
#lm 应用配置模块
#lm 功能定义accounts应用的配置信息包括应用名称等元数据
# 从Django的apps模块导入AppConfig类用于定义应用的配置信息
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""
accounts应用的配置类用于设置应用的基本信息
继承自Django的AppConfig通过此类可以配置应用的名称默认自动生成的主键类型等元数据
"""
#lm 定义应用的名称Django通过此名称识别该应用与项目settings.py中INSTALLED_APPS里的配置对应
name = 'accounts'

@ -1,57 +0,0 @@
#lm 自定义认证后端模块
#lm 功能:实现支持用户名或邮箱登录的自定义认证逻辑
#lm 扩展Django默认认证方式提供更灵活的用户身份验证
# 导入Django的用户模型工具用于获取项目配置的用户模型支持自定义用户模型
from django.contrib.auth import get_user_model
# 导入Django内置的模型认证后端基类用于扩展自定义认证逻辑
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端继承自Django的ModelBackend
功能允许用户使用用户名或邮箱地址进行登录验证
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
重写认证方法实现用户名/邮箱登录逻辑
:param request: 请求对象
:param username: 登录时输入的标识可能是用户名或邮箱
:param password: 登录密码
:param kwargs: 其他关键字参数
:return: 验证成功返回用户对象失败返回None
"""
#lm 判断输入的"username"是否包含@符号,若包含则视为邮箱登录
if '@' in username:
#lm 构建查询条件使用email字段匹配
kwargs = {'email': username}
else:
#lm 否则视为用户名登录构建查询条件使用username字段匹配
kwargs = {'username': username}
try:
#lm 根据构建的条件查询用户(使用项目配置的用户模型)
user = get_user_model().objects.get(**kwargs)
#lm 验证查询到的用户密码是否正确Django内置的密码校验自动处理哈希对比
if user.check_password(password):
#lm 密码正确,返回用户对象
return user
except get_user_model().DoesNotExist:
#lm 若用户不存在查询失败返回None表示认证失败
return None
def get_user(self, username):
"""
重写获取用户的方法根据用户主键获取用户对象
Django认证系统会调用此方法获取已认证用户的详细信息
:param username: 实际为用户的主键pk
:return: 存在则返回用户对象不存在返回None
"""
try:
#lm 根据主键查询用户
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
#lm 用户不存在返回None
return None

@ -1,157 +0,0 @@
#lm 用户表单定义模块
#lm 功能:定义用户相关的所有表单类,包括登录、注册、忘记密码等表单
#lm 提供表单验证、字段定义和界面展示控制
# 导入Django表单基类
from django import forms
# 导入Django用户模型工具及密码验证功能
from django.contrib.auth import get_user_model, password_validation
# 导入Django内置的认证表单登录、用户创建
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
# 导入Django验证错误类用于自定义表单验证
from django.core.exceptions import ValidationError
# 导入Django表单控件用于自定义输入框样式
from django.forms import widgets
# 导入翻译工具,实现字段名称/提示的国际化
from django.utils.translation import gettext_lazy as _
# 导入自定义工具类(可能用于验证码验证等)
from . import utils
# 导入自定义用户模型
from .models import BlogUser
class LoginForm(AuthenticationForm):
"""自定义登录表单继承自Django内置的AuthenticationForm"""
def __init__(self, *args, **kwargs):
"""初始化方法,重写父类初始化逻辑以自定义表单控件样式"""
super(LoginForm, self).__init__(*args, **kwargs)
#lm 自定义用户名字段的输入控件设置占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#lm 自定义密码字段的输入控件使用密码输入框设置占位符和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""自定义注册表单继承自Django内置的UserCreationForm"""
def __init__(self, *args, **kwargs):
"""初始化方法,重写父类初始化逻辑以自定义表单控件样式"""
super(RegisterForm, self).__init__(*args, **kwargs)
#lm 自定义用户名字段控件文本输入框设置占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#lm 自定义邮箱字段控件邮箱输入框设置占位符和CSS类
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
#lm 自定义密码1字段控件密码输入框设置占位符和CSS类
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
#lm 自定义密码2字段控件密码输入框确认密码设置占位符和CSS类
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""自定义邮箱验证:检查邮箱是否已被注册"""
email = self.cleaned_data['email']
#lm 如果该邮箱已存在于用户表中,抛出验证错误
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
#lm 关联的用户模型通过get_user_model获取项目配置的用户模型
model = get_user_model()
#lm 注册表单需填写的字段:用户名和邮箱
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
"""忘记密码表单,用于用户重置密码(包含密码重置、邮箱验证、验证码验证)"""
#lm 新密码字段标签国际化使用密码输入框设置CSS类和占位符
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
#lm 确认新密码字段:标签为"确认密码",使用密码输入框,设置样式
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
#lm 邮箱字段:用于验证用户身份,使用文本输入框,设置样式
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
#lm 验证码字段:用于验证用户真实性,使用文本输入框,设置样式
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
"""验证两次输入的新密码是否一致,并验证密码强度"""
password1 = self.data.get("new_password1") #lm 获取第一次输入的密码
password2 = self.data.get("new_password2") #lm 获取第二次输入的密码
#lm 如果两次密码都存在且不一致,抛出验证错误
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
#lm 使用Django内置密码验证器验证密码强度如长度、复杂度等
password_validation.validate_password(password2)
return password2
def clean_email(self):
"""验证邮箱是否已注册(存在于用户表中)"""
user_email = self.cleaned_data.get("email")
#lm 如果该邮箱不存在于BlogUser表中抛出验证错误
if not BlogUser.objects.filter(
email=user_email
).exists():
#lm 注意:此处提示可能暴露邮箱是否注册,可根据需求修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
"""验证验证码是否有效调用自定义工具类的verify方法"""
code = self.cleaned_data.get("code")
#lm 调用utils.verify验证邮箱和验证码是否匹配返回错误信息若有
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
#lm 如果验证失败(有错误信息),抛出验证错误
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
"""获取忘记密码验证码的表单,用于提交邮箱以发送验证码"""
#lm 邮箱字段:用于指定需要发送验证码的邮箱,标签国际化
email = forms.EmailField(
label=_('Email'),
)

@ -1,276 +0,0 @@
# 导入Django测试所需的核心类Client模拟HTTP请求、RequestFactory创建请求对象、TestCase测试基类
from django.test import Client, RequestFactory, TestCase
# 导入reverse通过URL名称生成路径用于测试中定位接口
from django.urls import reverse
# 导入timezone处理时间相关字段用于创建测试数据
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
# 定义账户相关测试类继承Django的TestCase提供测试框架支持
class AccountTest(TestCase):
def setUp(self):
"""
测试初始化方法在每个测试方法执行前自动调用
用于创建共用的测试对象避免代码重复
"""
# 创建测试客户端用于模拟用户发送HTTP请求
self.client = Client()
# 创建请求工厂,用于生成原始请求对象(按需使用)
self.factory = RequestFactory()
# 创建普通测试用户用户名test、邮箱admin@admin.com、密码12345678
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
# 定义测试用的新密码,后续忘记密码测试中使用
self.new_test = "xxx123--="
def test_validate_account(self):
"""测试账户基础功能超级用户创建、登录验证、admin访问、内容创建与管理"""
# 获取当前站点域名(测试中未实际使用,保持原逻辑)
site = get_current_site().domain
# 创建超级用户拥有admin管理权限
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
# 通过用户名查询刚创建的超级用户,验证创建成功
testuser = BlogUser.objects.get(username='liangliangyy1')
# 使用测试客户端模拟超级用户登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
# 断言登录成功返回True
self.assertEqual(loginresult, True)
# 模拟访问admin后台页面
response = self.client.get('/admin/')
# 断言admin页面访问成功状态码200
self.assertEqual(response.status_code, 200)
# 创建测试分类:设置名称、创建时间、修改时间并保存
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章:关联作者(超级用户)和分类,设置标题、内容、类型、状态并保存
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' # 假设'a'代表普通文章类型
article.status = 'p' # 假设'p'代表已发布状态
article.save()
# 模拟访问文章的admin管理页面通过文章模型的get_admin_url方法获取路径
response = self.client.get(article.get_admin_url())
# 断言文章管理页面访问成功状态码200
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
"""测试用户注册流程:注册请求、注册后用户存在性、邮箱验证、登录与权限升级、内容管理"""
# 注册前断言邮箱为user123@user.com的用户不存在初始状态
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 模拟发送注册请求向account:register接口提交用户名、邮箱、两次一致的密码
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
# 注册后断言邮箱为user123@user.com的用户存在注册成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 获取刚注册的用户,生成邮箱验证链接(使用项目加密逻辑生成签名)
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 双重SHA256加密签名
path = reverse('accounts:result') # 验证结果页的URL路径
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) # 拼接完整验证URL
# 模拟访问邮箱验证链接
response = self.client.get(url)
# 断言验证页面访问成功状态码200
self.assertEqual(response.status_code, 200)
# 模拟刚注册的用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 升级用户权限为超级用户便于测试admin功能
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True # 设为超级用户
user.is_staff = True # 允许登录admin
user.save()
# 删除侧边栏缓存(项目自定义缓存操作,测试中保持原逻辑)
delete_sidebar_cache()
# 创建测试分类(用于后续创建文章)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章(关联升级权限后的用户)
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
# 模拟访问文章的admin管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 模拟用户登出
response = self.client.get(reverse('account:logout'))
# 断言登出请求响应正常允许301/302重定向或200成功
self.assertIn(response.status_code, [301, 302, 200])
# 登出后尝试访问文章admin页面应被拒绝或重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 模拟使用错误密码登录(密码不匹配)
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
# 断言登录请求响应正常(无论成功失败,状态码合法)
self.assertIn(response.status_code, [301, 302, 200])
# 错误登录后尝试访问文章admin页面应被拒绝或重定向
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)
# 验证1使用正确的邮箱和验证码断言无错误返回验证成功
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# 验证2使用错误的邮箱与存储的验证码不匹配断言返回错误信息字符串类型
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
"""测试获取忘记密码验证码的成功场景:提交正确邮箱,返回成功响应"""
# 模拟向account:forget_password_code接口提交正确邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
# 断言请求成功状态码200且返回内容为"ok"(表示验证码发送成功)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
"""测试获取忘记密码验证码的失败场景:无邮箱、邮箱格式错误"""
# 失败场景1不提交邮箱空数据
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
# 断言返回"错误的邮箱"(参数缺失)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 失败场景2提交格式错误的邮箱admin@com不符合标准格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
# 断言返回"错误的邮箱"(格式校验失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""测试忘记密码重置成功场景:提交正确验证码和新密码,密码修改生效"""
# 生成验证码并关联测试用户的邮箱(模拟用户已获取验证码)
code = generate_code()
utils.set_code(self.blog_user.email, code)
# 构造忘记密码重置请求数据:新密码(两次一致)、用户邮箱、正确验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
# 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言重置成功重定向到结果页状态码302
self.assertEqual(resp.status_code, 302)
# 验证密码是否真的修改成功:查询用户并校验新密码
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # 获取用户实例
self.assertNotEqual(blog_user, None) # 断言用户存在
# 使用check_password方法验证新密码是否匹配Django内置密码校验自动处理哈希
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
"""测试忘记密码重置失败场景:使用不存在的邮箱"""
# 构造请求数据:新密码、不存在的邮箱、任意验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
# 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言请求响应正常页面返回错误提示状态码200
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
"""测试忘记密码重置失败场景:验证码错误"""
# 生成正确验证码并关联用户邮箱,但请求时提交错误验证码
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # 错误的验证码
)
# 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言请求响应正常页面返回验证码错误提示状态码200
self.assertEqual(resp.status_code, 200)

@ -1,280 +0,0 @@
#lm 用户账户功能测试模块
#lm 功能:包含用户注册、登录、密码重置等功能的自动化测试用例
#lm 确保用户相关功能的正确性和稳定性
# 导入Django测试所需的核心类Client模拟HTTP请求、RequestFactory创建请求对象、TestCase测试基类
from django.test import Client, RequestFactory, TestCase
# 导入reverse通过URL名称生成路径用于测试中定位接口
from django.urls import reverse
# 导入timezone处理时间相关字段用于创建测试数据
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
# 定义账户相关测试类继承Django的TestCase提供测试框架支持
class AccountTest(TestCase):
def setUp(self):
"""
测试初始化方法在每个测试方法执行前自动调用
用于创建共用的测试对象避免代码重复
"""
#lm 创建测试客户端用于模拟用户发送HTTP请求
self.client = Client()
#lm 创建请求工厂,用于生成原始请求对象(按需使用)
self.factory = RequestFactory()
#lm 创建普通测试用户用户名test、邮箱admin@admin.com、密码12345678
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
#lm 定义测试用的新密码,后续忘记密码测试中使用
self.new_test = "xxx123--="
def test_validate_account(self):
"""测试账户基础功能超级用户创建、登录验证、admin访问、内容创建与管理"""
#lm 获取当前站点域名(测试中未实际使用,保持原逻辑)
site = get_current_site().domain
#lm 创建超级用户拥有admin管理权限
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
#lm 通过用户名查询刚创建的超级用户,验证创建成功
testuser = BlogUser.objects.get(username='liangliangyy1')
#lm 使用测试客户端模拟超级用户登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
#lm 断言登录成功返回True
self.assertEqual(loginresult, True)
#lm 模拟访问admin后台页面
response = self.client.get('/admin/')
#lm 断言admin页面访问成功状态码200
self.assertEqual(response.status_code, 200)
#lm 创建测试分类:设置名称、创建时间、修改时间并保存
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#lm 创建测试文章:关联作者(超级用户)和分类,设置标题、内容、类型、状态并保存
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' #lm 假设'a'代表普通文章类型
article.status = 'p' #lm 假设'p'代表已发布状态
article.save()
#lm 模拟访问文章的admin管理页面通过文章模型的get_admin_url方法获取路径
response = self.client.get(article.get_admin_url())
#lm 断言文章管理页面访问成功状态码200
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
"""测试用户注册流程:注册请求、注册后用户存在性、邮箱验证、登录与权限升级、内容管理"""
#lm 注册前断言邮箱为user123@user.com的用户不存在初始状态
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lm 模拟发送注册请求向account:register接口提交用户名、邮箱、两次一致的密码
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
#lm 注册后断言邮箱为user123@user.com的用户存在注册成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lm 获取刚注册的用户,生成邮箱验证链接(使用项目加密逻辑生成签名)
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) #lm 双重SHA256加密签名
path = reverse('accounts:result') #lm 验证结果页的URL路径
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) #lm 拼接完整验证URL
#lm 模拟访问邮箱验证链接
response = self.client.get(url)
#lm 断言验证页面访问成功状态码200
self.assertEqual(response.status_code, 200)
#lm 模拟刚注册的用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
#lm 升级用户权限为超级用户便于测试admin功能
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True #lm 设为超级用户
user.is_staff = True #lm 允许登录admin
user.save()
#lm 删除侧边栏缓存(项目自定义缓存操作,测试中保持原逻辑)
delete_sidebar_cache()
#lm 创建测试分类(用于后续创建文章)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#lm 创建测试文章(关联升级权限后的用户)
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
#lm 模拟访问文章的admin管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#lm 模拟用户登出
response = self.client.get(reverse('account:logout'))
#lm 断言登出请求响应正常允许301/302重定向或200成功
self.assertIn(response.status_code, [301, 302, 200])
#lm 登出后尝试访问文章admin页面应被拒绝或重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
#lm 模拟使用错误密码登录(密码不匹配)
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
#lm 断言登录请求响应正常(无论成功失败,状态码合法)
self.assertIn(response.status_code, [301, 302, 200])
#lm 错误登录后尝试访问文章admin页面应被拒绝或重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""测试邮箱验证码验证逻辑:正确验证码验证成功,错误邮箱验证失败"""
#lm 定义测试邮箱和生成随机验证码
to_email = "admin@admin.com"
code = generate_code() #lm 调用工具函数生成验证码
#lm 存储验证码(关联邮箱,用于后续验证)
utils.set_code(to_email, code)
#lm 发送验证邮件(测试中仅执行流程,不实际校验邮件发送结果)
utils.send_verify_email(to_email, code)
#lm 验证1使用正确的邮箱和验证码断言无错误返回验证成功
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
#lm 验证2使用错误的邮箱与存储的验证码不匹配断言返回错误信息字符串类型
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
"""测试获取忘记密码验证码的成功场景:提交正确邮箱,返回成功响应"""
#lm 模拟向account:forget_password_code接口提交正确邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
#lm 断言请求成功状态码200且返回内容为"ok"(表示验证码发送成功)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
"""测试获取忘记密码验证码的失败场景:无邮箱、邮箱格式错误"""
#lm 失败场景1不提交邮箱空数据
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
#lm 断言返回"错误的邮箱"(参数缺失)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
#lm 失败场景2提交格式错误的邮箱admin@com不符合标准格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
#lm 断言返回"错误的邮箱"(格式校验失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""测试忘记密码重置成功场景:提交正确验证码和新密码,密码修改生效"""
#lm 生成验证码并关联测试用户的邮箱(模拟用户已获取验证码)
code = generate_code()
utils.set_code(self.blog_user.email, code)
#lm 构造忘记密码重置请求数据:新密码(两次一致)、用户邮箱、正确验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
#lm 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lm 断言重置成功重定向到结果页状态码302
self.assertEqual(resp.status_code, 302)
#lm 验证密码是否真的修改成功:查询用户并校验新密码
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() #lm 获取用户实例
self.assertNotEqual(blog_user, None) #lm 断言用户存在
#lm 使用check_password方法验证新密码是否匹配Django内置密码校验自动处理哈希
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
"""测试忘记密码重置失败场景:使用不存在的邮箱"""
#lm 构造请求数据:新密码、不存在的邮箱、任意验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
#lm 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lm 断言请求响应正常页面返回错误提示状态码200
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
"""测试忘记密码重置失败场景:验证码错误"""
#lm 生成正确验证码并关联用户邮箱,但请求时提交错误验证码
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", #lm 错误的验证码
)
#lm 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lm 断言请求响应正常页面返回验证码错误提示状态码200
self.assertEqual(resp.status_code, 200)

@ -1,49 +0,0 @@
#lm URL路由配置模块
#lm 功能定义用户账户相关的所有URL路径和对应的视图函数映射
#lm 包含登录、注册、登出、密码重置等功能的URL配置
# 导入Django的URL路径定义工具path用于精确路径匹配re_path支持正则表达式匹配
from django.urls import path
from django.urls import re_path
# 导入当前应用accounts的视图模块包含登录、注册等业务逻辑处理
from . import views
# 导入当前应用的自定义登录表单,用于登录页面的表单渲染和验证
from .forms import LoginForm
# 定义应用的命名空间为"accounts"避免不同应用间URL名称冲突
app_name = "accounts"
# URL路由配置列表映射URL路径到对应的视图
urlpatterns = [
#lm 1. 登录页面URL
re_path(r'^login/$', #lm 正则匹配路径:以"login/"开头并结束(即精确匹配"/login/"
views.LoginView.as_view(success_url='/'), #lm 关联LoginView视图登录成功后重定向到网站根路径"/"
name='login', #lm URL的命名用于模板或视图中通过reverse('accounts:login')生成路径
kwargs={'authentication_form': LoginForm}), #lm 传递参数指定登录使用自定义的LoginForm表单
#lm 2. 注册页面URL
re_path(r'^register/$', #lm 正则匹配路径:精确匹配"/register/"
views.RegisterView.as_view(success_url="/"), #lm 关联RegisterView视图注册成功后重定向到网站根路径
name='register'), #lm URL命名用于反向生成注册页面路径
#lm 3. 登出功能URL
re_path(r'^logout/$', #lm 正则匹配路径:精确匹配"/logout/"
views.LogoutView.as_view(), #lm 关联LogoutView视图处理登出逻辑默认登出后重定向到登录页
name='logout'), #lm URL命名用于反向生成登出路径
#lm 4. 账户操作结果页URL如登录/注册/密码重置后的结果提示)
path(r'account/result.html', #lm 精确路径匹配:固定路径"/account/result.html"
views.account_result, #lm 关联普通函数视图account_result处理结果页渲染
name='result'), #lm URL命名用于反向生成结果页路径
#lm 5. 忘记密码页面URL密码重置表单页
re_path(r'^forget_password/$', #lm 正则匹配路径:精确匹配"/forget_password/"
views.ForgetPasswordView.as_view(), #lm 关联ForgetPasswordView视图处理密码重置表单逻辑
name='forget_password'), #lm URL命名用于反向生成忘记密码页面路径
#lm 6. 获取忘记密码验证码的URL发送验证码到邮箱
re_path(r'^forget_password_code/$', #lm 正则匹配路径:精确匹配"/forget_password_code/"
views.ForgetPasswordEmailCode.as_view(), #lm 关联ForgetPasswordEmailCode视图处理发送验证码逻辑
name='forget_password_code'), #lm URL命名用于反向生成获取验证码的路径
]

@ -1,324 +0,0 @@
#lm 用户视图处理模块
#lm 功能处理用户相关的所有HTTP请求包括登录、注册、登出、密码重置等
#lm 包含类视图和函数视图,实现用户账户管理的核心业务逻辑
# 导入日志模块,用于记录视图操作中的关键信息(如请求类型、用户状态等)
import logging
# 导入Django国际化翻译工具用于视图中文本的多语言支持
from django.utils.translation import gettext_lazy as _
# 导入Django项目配置用于获取SECRET_KEY等全局设置
from django.conf import settings
# 导入Django认证相关模块auth处理登录逻辑REDIRECT_FIELD_NAME定义重定向参数名get_user_model获取自定义用户模型
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 # 登出功能函数
# 导入Django内置认证表单用于登录表单的基础验证
from django.contrib.auth.forms import AuthenticationForm
# 导入密码哈希工具,用于生成加密后的密码(忘记密码功能中更新密码时使用)
from django.contrib.auth.hashers import make_password
# 导入DjangoHTTP响应类重定向、403禁止访问、基础响应
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
# 导入Django快捷函数get_object_or_404获取对象不存在时返回404、render渲染模板
from django.shortcuts import get_object_or_404
from django.shortcuts import render
# 导入reverse函数通过URL名称生成路径避免硬编码路径
from django.urls import reverse
# 导入Django视图装饰器method_decorator为类视图方法添加装饰器、never_cache禁止缓存、csrf_protectCSRF保护、sensitive_post_parameters保护敏感POST参数
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme # 验证重定向地址是否安全
# 导入Django类视图基类View基础视图类、FormView处理表单的视图类、RedirectView处理重定向的视图类
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
# 导入项目自定义工具函数发送邮件、SHA256加密、获取当前站点、生成验证码、删除侧边栏缓存
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
# 导入当前应用accounts的工具函数验证码相关操作
from . import utils
# 导入当前应用的自定义表单:注册、登录、忘记密码、获取忘记密码验证码的表单
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
# 导入当前应用的自定义用户模型
from .models import BlogUser
# 创建日志记录器,用于记录当前视图模块的日志
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
"""
处理用户注册的类视图继承自FormView专门处理表单提交的视图基类
功能展示注册表单验证表单数据创建未激活用户发送邮箱验证链接跳转注册结果页
"""
form_class = RegisterForm #lm 关联的表单类自定义的RegisterForm
template_name = 'account/registration_form.html' #lm 渲染的模板文件路径
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
重写dispatch方法为视图添加CSRF保护装饰器
dispatch是类视图的入口方法所有请求GET/POST都会先经过此方法
"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""
表单数据验证通过后执行的逻辑用户提交的注册信息合法时
"""
if form.is_valid():
#lm 1. 创建用户但不立即提交到数据库commit=False后续手动设置额外字段
user = form.save(False)
#lm 2. 设置用户初始状态:未激活(需邮箱验证后激活)、注册来源为"Register"
user.is_active = False
user.source = 'Register'
#lm 3. 提交用户数据到数据库
user.save(True)
#lm 4. 生成邮箱验证链接包含当前站点域名、验证路径、用户ID、加密签名
site = get_current_site().domain #lm 获取当前站点域名如www.example.com
#lm 双重SHA256加密使用SECRET_KEY+用户ID生成签名防止链接被篡改
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#lm 开发环境DEBUG=True站点域名替换为本地地址127.0.0.1:8000
if settings.DEBUG:
site = '127.0.0.1:8000'
#lm 获取验证结果页的路径通过URL名称反向生成
path = reverse('account:result')
#lm 拼接完整的邮箱验证链接
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
#lm 5. 构造验证邮件内容(包含验证链接)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
#lm 6. 发送验证邮件到用户注册邮箱
send_email(
emailto=[user.email], #lm 收件人列表
title='验证您的电子邮箱', #lm 邮件标题
content=content #lm 邮件HTML内容
)
#lm 7. 重定向到注册结果页携带注册成功的类型和用户ID参数
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
#lm 表单验证失败(如用户名已存在、邮箱格式错误),重新渲染表单并显示错误信息
return self.render_to_response({'form': form})
class LogoutView(RedirectView):
"""
处理用户登出的类视图继承自RedirectView专门处理重定向的视图基类
功能执行登出逻辑删除侧边栏缓存重定向到登录页
"""
url = '/login/' #lm 登出后默认重定向的目标路径(登录页)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""
重写dispatch方法添加禁止缓存装饰器
避免浏览器缓存登出页面防止用户后退到已登出的页面
"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""
处理GET请求用户访问登出URL时
"""
#lm 1. 执行Django内置的登出函数清除用户的session信息
logout(request)
#lm 2. 删除侧边栏缓存(可能存储了用户相关信息,登出后需更新)
delete_sidebar_cache()
#lm 3. 调用父类的get方法执行重定向到登录页
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""
处理用户登录的类视图继承自FormView
功能展示登录表单验证登录信息处理"记住我"功能重定向到目标页面
"""
form_class = LoginForm #lm 关联的表单类自定义的LoginForm
template_name = 'account/login.html' #lm 渲染的登录模板路径
success_url = '/' #lm 登录成功后的默认重定向路径(网站根目录)
redirect_field_name = REDIRECT_FIELD_NAME #lm 重定向参数名(默认是"next"
login_ttl = 2626560 #lm 记住登录状态的有效期(秒),约等于一个月
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""
重写dispatch方法添加三个装饰器
1. sensitive_post_parameters('password')保护密码参数不在错误日志中显示
2. csrf_protect开启CSRF保护防止跨站请求伪造
3. never_cache禁止缓存登录页面确保每次访问都是最新状态
"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
扩展上下文数据将重定向地址next参数传递到模板中
模板中可根据该参数决定登录成功后跳转到哪里
"""
#lm 从GET请求中获取重定向地址如访问需要登录的页面时会携带next参数
redirect_to = self.request.GET.get(self.redirect_field_name)
#lm 如果没有重定向地址,默认设置为网站根目录
if redirect_to is None:
redirect_to = '/'
#lm 将重定向地址添加到上下文
kwargs['redirect_to'] = redirect_to
#lm 调用父类方法,返回完整的上下文数据
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""
表单数据验证通过后执行的逻辑用户提交的登录信息合法时
注意此处重新初始化了AuthenticationForm用于Django内置的登录验证
"""
#lm 用请求的POST数据和request对象初始化Django内置的认证表单
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
#lm 1. 登录成功,删除侧边栏缓存(可能存储了未登录状态的内容)
delete_sidebar_cache()
#lm 2. 记录重定向参数名到日志(用于调试)
logger.info(self.redirect_field_name)
#lm 3. 执行Django内置的登录函数将用户信息存入session
auth.login(self.request, form.get_user())
#lm 4. 处理"记住我"功能:如果用户勾选了"remember"设置session有效期
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
#lm 5. 调用父类的form_valid方法执行重定向到成功页面
return super(LoginView, self).form_valid(form)
else:
#lm 表单验证失败(如用户名不存在、密码错误),重新渲染表单并显示错误
return self.render_to_response({'form': form})
def get_success_url(self):
"""
自定义登录成功后的重定向地址
优先使用POST请求中的"next"参数如果合法否则使用默认的success_url
"""
#lm 从POST请求中获取重定向地址用户登录时提交的next参数
redirect_to = self.request.POST.get(self.redirect_field_name)
#lm 验证重定向地址是否安全(是否属于当前站点,防止恶意重定向)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]):
#lm 不安全的地址,使用默认的成功重定向路径
redirect_to = self.success_url
return redirect_to
def account_result(request):
"""
处理账户操作结果的函数视图注册成功提示邮箱验证结果
功能根据URL参数type和id展示不同的结果信息激活用户邮箱
"""
#lm 从GET请求中获取操作类型register/validation和用户ID
type = request.GET.get('type')
id = request.GET.get('id')
#lm 根据用户ID查询用户不存在则返回404页面
user = get_object_or_404(get_user_model(), id=id)
#lm 记录操作类型到日志
logger.info(type)
#lm 如果用户已激活is_active=True直接重定向到根目录无需再展示结果
if user.is_active:
return HttpResponseRedirect('/')
#lm 如果操作类型合法属于register或validation处理对应的逻辑
if type and type in ['register', 'validation']:
if type == 'register':
#lm 1. 注册成功场景:展示注册成功提示,告知用户验证邮件已发送
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
#lm 2. 邮箱验证场景:验证签名是否合法,合法则激活用户
#lm 重新生成签名用于与请求中的sign参数对比
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#lm 从GET请求中获取签名参数
sign = request.GET.get('sign')
#lm 如果签名不匹配链接被篡改返回403禁止访问
if sign != c_sign:
return HttpResponseForbidden()
#lm 签名匹配激活用户设置is_active=True并保存
user.is_active = True
user.save()
#lm 展示验证成功提示
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
#lm 渲染结果页面,传递标题和内容
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
#lm 操作类型不合法,重定向到根目录
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
"""
处理忘记密码的类视图继承自FormView
功能展示忘记密码表单验证表单数据验证码邮箱密码更新用户密码
"""
form_class = ForgetPasswordForm #lm 关联的表单类自定义的ForgetPasswordForm
template_name = 'account/forget_password.html' #lm 渲染的忘记密码模板路径
def form_valid(self, form):
"""
表单数据验证通过后执行的逻辑验证码邮箱密码均合法时
"""
if form.is_valid():
#lm 1. 根据表单中的邮箱查询对应的用户
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
#lm 2. 用新密码(加密处理)更新用户密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
#lm 3. 保存密码更新结果到数据库
blog_user.save()
#lm 4. 重定向到登录页(密码重置成功后需重新登录)
return HttpResponseRedirect('/login/')
else:
#lm 表单验证失败(如验证码错误、密码不一致),重新渲染表单并显示错误
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""
处理发送忘记密码验证码的类视图继承自基础View类
功能接收邮箱验证邮箱格式生成验证码发送验证邮件存储验证码到缓存
"""
def post(self, request: HttpRequest):
"""
处理POST请求用户提交邮箱以获取验证码时
"""
#lm 1. 用请求的POST数据初始化验证码表单
form = ForgetPasswordCodeForm(request.POST)
#lm 2. 验证表单(主要验证邮箱格式是否合法)
if not form.is_valid():
#lm 表单验证失败(邮箱格式错误),返回"错误的邮箱"响应
return HttpResponse("错误的邮箱")
#lm 3. 表单验证通过,获取清洗后的邮箱地址
to_email = form.cle

@ -1,91 +0,0 @@
#lm 用户管理后台配置模块
#lm 功能定义Django admin后台中用户管理的自定义表单和管理员配置
#lm 包含用户创建表单、用户修改表单和用户管理员类的自定义实现
# 导入Django表单基类
from django import forms
# 导入Django内置的用户管理员类用于扩展自定义用户的admin配置
from django.contrib.auth.admin import UserAdmin
# 导入Django内置的用户修改表单用于扩展自定义用户的修改表单
from django.contrib.auth.forms import UserChangeForm
# 导入Django内置的用户名字段类用于自定义用户名字段验证
from django.contrib.auth.forms import UsernameField
# 导入翻译工具,用于实现字段名称的国际化
from django.utils.translation import gettext_lazy as _
# 注册模型时需要导入自定义的用户模型
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""自定义用户创建表单用于在admin站点添加新用户时使用"""
#lm 密码字段,使用密码输入框(输入内容隐藏),标签支持国际化
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
#lm 确认密码字段,用于验证两次输入的密码是否一致
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
#lm 关联的模型为自定义的BlogUser
model = BlogUser
#lm 创建用户时需要填写的字段这里仅指定email其他字段可默认或后续补充
fields = ('email',)
def clean_password2(self):
"""验证两次输入的密码是否一致"""
#lm 获取清洗后的密码1和密码2
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
#lm 如果两次密码都存在且不一致,抛出验证错误
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
"""保存用户,将密码加密处理,并设置创建来源"""
#lm 调用父类的save方法先不提交到数据库commit=False
user = super().save(commit=False)
#lm 对密码进行加密处理Django内置的密码哈希方法
user.set_password(self.cleaned_data["password1"])
#lm 如果需要提交到数据库
if commit:
#lm 设置用户的创建来源为'adminsite'表示通过admin站点创建
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
"""自定义用户修改表单用于在admin站点编辑用户信息时使用"""
class Meta:
#lm 关联的模型为BlogUser
model = BlogUser
#lm 显示所有字段(可根据需要指定具体字段)
fields = '__all__'
#lm 指定用户名字段的处理类为UsernameField提供内置验证
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
"""初始化方法,调用父类的初始化逻辑"""
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""自定义用户管理员类用于在admin站点配置BlogUser的展示和操作"""
#lm 指定修改用户时使用的表单
form = BlogUserChangeForm
#lm 指定添加用户时使用的表单
add_form = BlogUserCreationForm
#lm 列表页展示的字段
list_display = (
'id', # 用户ID
'nickname', # 昵称
'username', # 用户名
'email', # 邮箱
'last_login', # 最后登录时间
'date_joined', # 注册时间
'source' # 创建来源
)
#lm 列表页中可点击跳转详情页的字段
list_display_links = ('id', 'username')
#lm 列表页的排序方式按ID降序即最新创建的用户在前
ordering = ('-id',)

@ -1,15 +0,0 @@
#lm 应用配置模块
#lm 功能定义accounts应用的配置信息包括应用名称等元数据
# 从Django的apps模块导入AppConfig类用于定义应用的配置信息
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""
accounts应用的配置类用于设置应用的基本信息
继承自Django的AppConfig通过此类可以配置应用的名称默认自动生成的主键类型等元数据
"""
#lm 定义应用的名称Django通过此名称识别该应用与项目settings.py中INSTALLED_APPS里的配置对应
name = 'accounts'

@ -1,57 +0,0 @@
#lm 自定义认证后端模块
#lm 功能:实现支持用户名或邮箱登录的自定义认证逻辑
#lm 扩展Django默认认证方式提供更灵活的用户身份验证
# 导入Django的用户模型工具用于获取项目配置的用户模型支持自定义用户模型
from django.contrib.auth import get_user_model
# 导入Django内置的模型认证后端基类用于扩展自定义认证逻辑
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端继承自Django的ModelBackend
功能允许用户使用用户名或邮箱地址进行登录验证
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
重写认证方法实现用户名/邮箱登录逻辑
:param request: 请求对象
:param username: 登录时输入的标识可能是用户名或邮箱
:param password: 登录密码
:param kwargs: 其他关键字参数
:return: 验证成功返回用户对象失败返回None
"""
#lm 判断输入的"username"是否包含@符号,若包含则视为邮箱登录
if '@' in username:
#lm 构建查询条件使用email字段匹配
kwargs = {'email': username}
else:
#lm 否则视为用户名登录构建查询条件使用username字段匹配
kwargs = {'username': username}
try:
#lm 根据构建的条件查询用户(使用项目配置的用户模型)
user = get_user_model().objects.get(**kwargs)
#lm 验证查询到的用户密码是否正确Django内置的密码校验自动处理哈希对比
if user.check_password(password):
#lm 密码正确,返回用户对象
return user
except get_user_model().DoesNotExist:
#lm 若用户不存在查询失败返回None表示认证失败
return None
def get_user(self, username):
"""
重写获取用户的方法根据用户主键获取用户对象
Django认证系统会调用此方法获取已认证用户的详细信息
:param username: 实际为用户的主键pk
:return: 存在则返回用户对象不存在返回None
"""
try:
#lm 根据主键查询用户
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
#lm 用户不存在返回None
return None

@ -0,0 +1,72 @@
#zjy 评论模块的Django Admin配置
#zjy 自定义评论在Django后台的管理界面
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 _
#zjy 批量禁用评论(将 is_enable 设为 False
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
#zjy 批量启用评论(将 is_enable 设为 True
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
#zjy 定义动作名称,在 Django Admin 批量操作菜单中显示的文字
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
#zjy 每页显示评论数量
list_per_page = 20
#zjy 在评论列表中显示哪些字段
list_display = (
'id',
'body', #zjy 评论正文
'link_to_userinfo', #zjy 用户信息(带链接)
'link_to_article', #zjy 所属文章(带链接)
'is_enable', #zjy 是否启用
'creation_time' #zjy 创建时间
)
#zjy 可以点击哪些字段进入编辑页面
list_display_links = ('id', 'body', 'is_enable')
#zjy 过滤器(后台右侧筛选功能)
list_filter = ('is_enable',)
#zjy 排除不需要在后台编辑的字段(自动时间字段不应手动修改)
exclude = ('creation_time', 'last_modify_time')
#zjy 批量操作按钮
actions = [disable_commentstatus, enable_commentstatus]
#zjy 显示用户信息,并可点击跳转到用户编辑页面
def link_to_userinfo(self, obj):
#zjy 获取目标 admin change 页面的 URL 路径
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
#zjy 显示用户昵称,若无昵称显示 email
return format_html(
'<a href="{}">{}</a>'.format(
link,
obj.author.nickname if obj.author.nickname else obj.author.email
)
)
#zjy 显示所属文章,并可点击跳转到文章编辑页面
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html('<a href="{}">{}</a>'.format(link, obj.article.title))
#zjy 设置在后台列表中显示的列标题
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -0,0 +1,13 @@
#zjy 评论模块的应用配置
#zjy 定义评论应用的配置信息
from django.apps import AppConfig
class CommentsConfig(AppConfig):
#zjy 指定该 App 在项目中的名称(即所在目录名)
name = 'comments'
#zjy (可选)可以在这里做初始化操作,如引入 signals
# def ready(self):
# import comments.signals

@ -0,0 +1,24 @@
#zjy 评论模块的表单定义
#zjy 定义评论提交的表单验证规则
from django import forms
from django.forms import ModelForm
from .models import Comment
class CommentForm(ModelForm):
#zjy 用于存储父评论的 ID实现评论回复功能
#zjy 该字段不会显示到页面中HiddenInput允许为空一级评论时为空
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput,
required=False
)
class Meta:
#zjy 指定该表单操作的模型为 Comment
model = Comment
#zjy 只允许用户输入评论内容body
#zjy 其他字段(如 author、article、parent_comment将在视图中自动赋值
fields = ['body']

@ -0,0 +1,65 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
# 表示这是该 app 的第一个迁移文件
initial = True
dependencies = [
# 依赖 blog 应用的第一条迁移文件,确保 Article 模型已经被创建
('blog', '0001_initial'),
# 依赖 Django 的用户模型(可自定义 AUTH_USER_MODEL
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
# 创建 Comment 模型
migrations.CreateModel(
name='Comment',
fields=[
# 主键 id自动递增
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 评论内容,最大长度 300 字
('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='是否显示')),
# 评论所属文章,一个评论只能属于一篇文章
# CASCADE 表示当文章删除时,该评论也会被删除
('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='上级评论')),
],
options={
# 后台显示名
'verbose_name': '评论',
'verbose_name_plural': '评论',
# 查询时默认按 id 倒序排列(新的评论排最前)
'ordering': ['-id'],
# get_latest_by 用于 Django 的 latest() 方法
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
# 当前迁移文件依赖于 comments 应用的 0001 初始迁移文件
dependencies = [
('comments', '0001_initial'),
]
operations = [
# 修改 Comment 模型中 is_enable 字段的属性
migrations.AlterField(
model_name='comment', # 要修改的模型名称
name='is_enable', # 要修改的字段名
field=models.BooleanField(
default=False, # 将默认值改为 False即默认评论不显示
verbose_name='是否显示' # 后台显示名称
),
),
]

@ -0,0 +1,111 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
# 迁移依赖顺序,确保其他相关模型先完成迁移
dependencies = [
# 依赖用户模型(可自定义 AUTH_USER_MODEL
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
# 依赖 blog 应用的某次迁移文件
('blog', '0005_alter_article_options_alter_category_options_and_more'),
# 依赖 comments 应用之前的迁移调整(包括 is_enable 字段的变更)
('comments', '0002_alter_comment_is_enable'),
]
operations = [
# 修改模型的元选项Meta 类中的配置)
migrations.AlterModelOptions(
name='comment',
options={
'get_latest_by': 'id', # 使用 id 作为 latest() 的默认排序依据
'ordering': ['-id'], # 查询结果默认按 id 降序排列(新评论在前)
'verbose_name': 'comment', # 后台显示名称(单数)
'verbose_name_plural': 'comment', # 后台显示名称(复数)
},
),
# 删除原来用于记录创建时间的字段 created_time
migrations.RemoveField(
model_name='comment',
name='created_time',
),
# 删除原来用于记录修改时间的字段 last_mod_time
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
# 新增评论创建时间字段(命名和 verbose_name 英文化)
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='creation time',
),
),
# 新增评论最后修改时间字段(命名和 verbose_name 英文化)
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='last modify time',
),
),
# 修改外键 article 字段的 verbose_name 显示文本
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 文章删除时,本评论也删除
to='blog.article',
verbose_name='article',
),
),
# 修改外键 author 字段的 verbose_name 显示文本
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 用户删除时,评论也删除
to=settings.AUTH_USER_MODEL,
verbose_name='author',
),
),
# 修改评论是否显示字段,将 verbose_name 英文化,默认不显示
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(
default=False,
verbose_name='enable', # 从中文变为英文显示
),
),
# 修改父评论字段(用于实现评论回复结构)
migrations.AlterField(
model_name='comment',
name='parent_comment',
field=models.ForeignKey(
blank=True, # 表单中允许为空
null=True, # 数据库中允许为 null
on_delete=django.db.models.deletion.CASCADE, # 父评论删除时子评论也删除
to='comments.comment',
verbose_name='parent comment',
),
),
]

@ -0,0 +1,74 @@
#zjy 评论模块的数据模型定义
#zjy 定义了Comment模型用于存储博客文章的评论信息支持多级回复功能
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
class Comment(models.Model):
#zjy 评论正文,限制最大输入长度 300使用 TextField 以便输入多行内容
body = models.TextField('正文', max_length=300)
#zjy 评论创建时间,默认使用 timezone.now可自动获取当前时区时间
creation_time = models.DateTimeField(
_('creation time'),
default=now
)
#zjy 评论最后修改时间,通常用于编辑评论功能,但如果不编辑也会保持不变
last_modify_time = models.DateTimeField(
_('last modify time'),
default=now
)
#zjy 评论作者,关联到用户模型,用户删除时,对应评论也一并删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE
)
#zjy 评论所属文章,关联到 Article 模型,文章被删除时,其下所有评论也被删除
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE
)
#zjy 父评论,用于构建"评论回复"树结构
#zjy 若为空 → 表示为一级评论;不为空 → 表示为某条评论的子评论
parent_comment = models.ForeignKey(
'self', #zjy 自关联
verbose_name=_('parent comment'),
blank=True, #zjy 表单中允许为空
null=True, #zjy 数据库允许为 null
on_delete=models.CASCADE #zjy 父评论删除时,子评论也被删除
)
#zjy 是否启用评论(常用于需要审核评论是否展示)
#zjy 默认为 False → 新评论不会立刻显示,需要管理员审核启用
is_enable = models.BooleanField(
_('enable'),
default=False,
blank=False,
null=False
)
class Meta:
#zjy 默认按 id 倒序排列 → 新评论显示在前
ordering = ['-id']
#zjy Django Admin 后台显示的模型名称
verbose_name = _('comment')
verbose_name_plural = verbose_name
#zjy latest() 方法依据 id 获取最新对象
get_latest_by = 'id'
def __str__(self):
#zjy 后台及 shell 打印对象时显示评论内容
return self.body

@ -0,0 +1,55 @@
from django import template
# 注册一个自定义标签库
register = template.Library()
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""
获取指定评论的所有子评论包括多级递归子评论
用法示例在模板中:
{% parse_commenttree article_comments comment as childcomments %}
参数解释
commentlist所有评论的查询集合一般是 article.comments.all()
comment当前评论对象
返回值
datas按层级顺序递归展开的所有子评论列表
"""
datas = [] # 用于存储递归解析得到的子评论
def parse(c):
# 找到当前评论 c 的直接子评论(过滤掉未启用的)
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
datas.append(child) # 保存子评论
parse(child) # 递归查找子评论的子评论
parse(comment)
return datas # 返回递归展开的所有子级评论
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""
渲染评论项组件
用法示例在模板中
{% show_comment_item comment True %}
参数
comment需要渲染的评论对象
ischild是否为子评论用于模板样式控制如缩进/层级
depth 解释
depth = 1 子评论缩进更深
depth = 2 顶级评论缩进较浅
"""
depth = 1 if ischild else 2
return {
'comment_item': comment, # 提供给模板的评论对象
'depth': depth # 让模板根据层级调整样式
}

@ -0,0 +1,131 @@
#zjy 评论模块的测试用例
#zjy 测试评论的提交、审核、显示和邮件通知等功能
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
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
class CommentsTest(TransactionTestCase):
#zjy 评论相关功能测试类
#zjy 使用 TransactionTestCase 允许测试包含事务的数据库操作
def setUp(self):
#zjy 测试初始化工作:
#zjy - 创建请求客户端
#zjy - 配置博客系统为"评论需要审核"
#zjy - 创建一个超级管理员用户(用于登录发表评论)
self.client = Client()
self.factory = RequestFactory()
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True #zjy 开启评论审核,提交的评论默认不显示
value.save()
#zjy 创建可登录的超级管理员用户
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1"
)
def update_article_comment_status(self, article):
#zjy 将文章下所有评论改为 is_enable=True
#zjy 模拟管理员审核通过评论(使评论显示)
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
def test_validate_comment(self):
#zjy 测试评论提交、审核、评论树解析等功能流程
#zjy 登录用户
self.client.login(username='liangliangyy1', password='liangliangyy1')
#zjy 创建分类
category = Category()
category.name = "categoryccc"
category.save()
#zjy 创建文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
#zjy 文章评论提交 URL
comment_url = reverse('comments:postcomment', kwargs={'article_id': article.id})
#zjy 用户提交第一条评论
response = self.client.post(comment_url, {'body': '123ffffffffff'})
self.assertEqual(response.status_code, 302) #zjy 正常应重定向(提交成功)
#zjy 因为评论需要审核,未批准前评论数为 0
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
#zjy 模拟管理员审核评论
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
#zjy 提交第二条评论
response = self.client.post(comment_url, {'body': '123ffffffffff'})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
#zjy 回复第一条评论(测试 parent_comment 功能)
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)
#zjy 通过审核
self.update_article_comment_status(article)
#zjy 再次获取文章
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
#zjy 测试评论树解析
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1) #zjy 第一条评论应当有 1 个子评论
#zjy 渲染评论组件标签是否正常返回
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
#zjy 测试工具函数获取最大文章/评论 id
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
#zjy 测试发送评论邮件通知(若配置邮件服务则会成功)
from comments.utils import send_comment_email
send_comment_email(comment)
send_comment_email(comment)

@ -0,0 +1,42 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.views.generic import View
from blog.models import Article
from .models import Comment
from .forms import CommentForm
class CommentPostView(LoginRequiredMixin, View):
"""
负责处理评论提交
"""
def post(self, request, article_id):
# 获取目标文章
article = get_object_or_404(Article, pk=article_id)
form = CommentForm(request.POST)
if form.is_valid():
# 获取评论内容
body = form.cleaned_data['body'].strip()
parent_id = form.cleaned_data.get('parent_comment_id')
comment = Comment()
comment.article = article
comment.author = request.user
comment.body = body
# 判断是否是子评论(回复)
if parent_id:
try:
parent_comment = Comment.objects.get(id=parent_id)
comment.parent_comment = parent_comment
except Comment.DoesNotExist:
pass
comment.save()
# 评论成功后返回文章页面
return HttpResponseRedirect(article.get_absolute_url())

@ -0,0 +1,69 @@
#zjy 评论模块的工具函数
#zjy 提供评论相关的辅助功能,如邮件通知
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
logger = logging.getLogger(__name__)
def send_comment_email(comment):
#zjy 当用户发表评论后,给评论者发送邮件通知;
#zjy 如果该评论是回复别人的,则同时给被回复的用户发送提醒邮件。
#zjy 获取当前站点域名
site = get_current_site().domain
#zjy 邮件主题
subject = _('Thanks for your comment')
#zjy 构造文章访问 URL以便用户点进查看
article_url = f"https://{site}{comment.article.get_absolute_url()}"
#zjy 给评论者自己发送的邮件内容
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
}
#zjy 收件人 = 评论的作者本人
tomail = comment.author.email
#zjy 发送邮件
send_email([tomail], subject, html_content)
try:
#zjy 如果该评论存在父评论(说明是回复行为)
if comment.parent_comment:
#zjy 给被回复的人发送通知邮件
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/>
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
}
#zjy 父评论的作者邮箱
tomail = comment.parent_comment.author.email
#zjy 向被回复者发送邮件
send_email([tomail], subject, html_content)
except Exception as e:
#zjy 出现错误则记录日志,但不影响评论正常流程
logger.error(e)

@ -0,0 +1,44 @@
#zjy 评论模块的视图处理
#zjy 处理评论的提交和显示逻辑
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.views.generic import View
from blog.models import Article
from .models import Comment
from .forms import CommentForm
class CommentPostView(LoginRequiredMixin, View):
#zjy 负责处理评论提交
#zjy 使用类视图方式处理POST请求
def post(self, request, article_id):
#zjy 获取目标文章
article = get_object_or_404(Article, pk=article_id)
form = CommentForm(request.POST)
if form.is_valid():
#zjy 获取评论内容
body = form.cleaned_data['body'].strip()
parent_id = form.cleaned_data.get('parent_comment_id')
comment = Comment()
comment.article = article
comment.author = request.user
comment.body = body
#zjy 判断是否是子评论(回复)
if parent_id:
try:
parent_comment = Comment.objects.get(id=parent_id)
comment.parent_comment = parent_comment
except Comment.DoesNotExist:
pass
comment.save()
#zjy 评论成功后返回文章页面
return HttpResponseRedirect(article.get_absolute_url())

@ -1,157 +0,0 @@
#lm 用户表单定义模块
#lm 功能:定义用户相关的所有表单类,包括登录、注册、忘记密码等表单
#lm 提供表单验证、字段定义和界面展示控制
# 导入Django表单基类
from django import forms
# 导入Django用户模型工具及密码验证功能
from django.contrib.auth import get_user_model, password_validation
# 导入Django内置的认证表单登录、用户创建
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
# 导入Django验证错误类用于自定义表单验证
from django.core.exceptions import ValidationError
# 导入Django表单控件用于自定义输入框样式
from django.forms import widgets
# 导入翻译工具,实现字段名称/提示的国际化
from django.utils.translation import gettext_lazy as _
# 导入自定义工具类(可能用于验证码验证等)
from . import utils
# 导入自定义用户模型
from .models import BlogUser
class LoginForm(AuthenticationForm):
"""自定义登录表单继承自Django内置的AuthenticationForm"""
def __init__(self, *args, **kwargs):
"""初始化方法,重写父类初始化逻辑以自定义表单控件样式"""
super(LoginForm, self).__init__(*args, **kwargs)
#lm 自定义用户名字段的输入控件设置占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#lm 自定义密码字段的输入控件使用密码输入框设置占位符和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""自定义注册表单继承自Django内置的UserCreationForm"""
def __init__(self, *args, **kwargs):
"""初始化方法,重写父类初始化逻辑以自定义表单控件样式"""
super(RegisterForm, self).__init__(*args, **kwargs)
#lm 自定义用户名字段控件文本输入框设置占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#lm 自定义邮箱字段控件邮箱输入框设置占位符和CSS类
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
#lm 自定义密码1字段控件密码输入框设置占位符和CSS类
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
#lm 自定义密码2字段控件密码输入框确认密码设置占位符和CSS类
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""自定义邮箱验证:检查邮箱是否已被注册"""
email = self.cleaned_data['email']
#lm 如果该邮箱已存在于用户表中,抛出验证错误
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
#lm 关联的用户模型通过get_user_model获取项目配置的用户模型
model = get_user_model()
#lm 注册表单需填写的字段:用户名和邮箱
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
"""忘记密码表单,用于用户重置密码(包含密码重置、邮箱验证、验证码验证)"""
#lm 新密码字段标签国际化使用密码输入框设置CSS类和占位符
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
#lm 确认新密码字段:标签为"确认密码",使用密码输入框,设置样式
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
#lm 邮箱字段:用于验证用户身份,使用文本输入框,设置样式
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
#lm 验证码字段:用于验证用户真实性,使用文本输入框,设置样式
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
"""验证两次输入的新密码是否一致,并验证密码强度"""
password1 = self.data.get("new_password1") #lm 获取第一次输入的密码
password2 = self.data.get("new_password2") #lm 获取第二次输入的密码
#lm 如果两次密码都存在且不一致,抛出验证错误
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
#lm 使用Django内置密码验证器验证密码强度如长度、复杂度等
password_validation.validate_password(password2)
return password2
def clean_email(self):
"""验证邮箱是否已注册(存在于用户表中)"""
user_email = self.cleaned_data.get("email")
#lm 如果该邮箱不存在于BlogUser表中抛出验证错误
if not BlogUser.objects.filter(
email=user_email
).exists():
#lm 注意:此处提示可能暴露邮箱是否注册,可根据需求修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
"""验证验证码是否有效调用自定义工具类的verify方法"""
code = self.cleaned_data.get("code")
#lm 调用utils.verify验证邮箱和验证码是否匹配返回错误信息若有
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
#lm 如果验证失败(有错误信息),抛出验证错误
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
"""获取忘记密码验证码的表单,用于提交邮箱以发送验证码"""
#lm 邮箱字段:用于指定需要发送验证码的邮箱,标签国际化
email = forms.EmailField(
label=_('Email'),
)

Binary file not shown.

Binary file not shown.

@ -1,68 +0,0 @@
#lm 用户数据模型模块
#lm 功能定义自定义用户模型BlogUser扩展Django内置用户模型功能
#lm 包含用户基本信息字段和相关的业务方法
# 导入Django内置的抽象用户模型用于扩展自定义用户功能包含基础用户名、密码等字段
from django.contrib.auth.models import AbstractUser
# 导入Django模型相关类用于定义数据库表结构
from django.db import models
# 导入reverse函数用于通过URL名称生成对应的URL路径
from django.urls import reverse
# 导入now函数用于获取当前时区的时间作为字段默认值
from django.utils.timezone import now
# 导入翻译工具,用于实现模型字段名称的国际化
from django.utils.translation import gettext_lazy as _
# 导入自定义工具函数get_current_site用于获取当前站点的域名信息
from djangoblog.utils import get_current_site
class BlogUser(AbstractUser):
"""
自定义用户模型继承自Django的AbstractUser
扩展了内置用户模型增加了昵称创建时间修改时间创建来源等自定义字段
"""
#lm 昵称字段支持国际化标签最大长度100允许空白不强制填写
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
#lm 创建时间字段支持国际化标签默认值为当前时间调用now函数
creation_time = models.DateTimeField(_('creation time'), default=now)
#lm 最后修改时间字段:支持国际化标签,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#lm 创建来源字段记录用户创建的渠道如adminsite、frontend等支持国际化标签允许空白
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""
定义模型实例的绝对URL标准Django方法
通过URL名称'blog:author_detail'生成用户详情页的URL参数为用户名
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username}) #lm kwargs传递URL所需的用户名参数
def __str__(self):
"""
定义模型实例的字符串表示
当打印或引用用户实例时返回用户的邮箱地址便于识别
"""
return self.email
def get_full_url(self):
"""
生成用户详情页的完整URL包含站点域名
结合当前站点域名和get_absolute_url生成的相对路径组成完整链接
"""
#lm 获取当前站点的域名如www.example.com
site = get_current_site().domain
#lm 拼接域名和相对路径形成完整URL使用HTTPS协议
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
"""
模型的元数据配置用于定义模型的显示和行为规则
"""
ordering = ['-id'] #lm 数据查询时的默认排序按ID降序最新创建的用户在前
verbose_name = _('user') #lm 模型的单数显示名称(支持国际化)
verbose_name_plural = verbose_name #lm 模型的复数显示名称(与单数一致,避免英文复数变形问题)
get_latest_by = 'id' #lm 获取"最新记录"时的依据字段按ID判断ID最大的为最新

@ -1,276 +0,0 @@
# 导入Django测试所需的核心类Client模拟HTTP请求、RequestFactory创建请求对象、TestCase测试基类
from django.test import Client, RequestFactory, TestCase
# 导入reverse通过URL名称生成路径用于测试中定位接口
from django.urls import reverse
# 导入timezone处理时间相关字段用于创建测试数据
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
# 定义账户相关测试类继承Django的TestCase提供测试框架支持
class AccountTest(TestCase):
def setUp(self):
"""
测试初始化方法在每个测试方法执行前自动调用
用于创建共用的测试对象避免代码重复
"""
# 创建测试客户端用于模拟用户发送HTTP请求
self.client = Client()
# 创建请求工厂,用于生成原始请求对象(按需使用)
self.factory = RequestFactory()
# 创建普通测试用户用户名test、邮箱admin@admin.com、密码12345678
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
# 定义测试用的新密码,后续忘记密码测试中使用
self.new_test = "xxx123--="
def test_validate_account(self):
"""测试账户基础功能超级用户创建、登录验证、admin访问、内容创建与管理"""
# 获取当前站点域名(测试中未实际使用,保持原逻辑)
site = get_current_site().domain
# 创建超级用户拥有admin管理权限
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
# 通过用户名查询刚创建的超级用户,验证创建成功
testuser = BlogUser.objects.get(username='liangliangyy1')
# 使用测试客户端模拟超级用户登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
# 断言登录成功返回True
self.assertEqual(loginresult, True)
# 模拟访问admin后台页面
response = self.client.get('/admin/')
# 断言admin页面访问成功状态码200
self.assertEqual(response.status_code, 200)
# 创建测试分类:设置名称、创建时间、修改时间并保存
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章:关联作者(超级用户)和分类,设置标题、内容、类型、状态并保存
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' # 假设'a'代表普通文章类型
article.status = 'p' # 假设'p'代表已发布状态
article.save()
# 模拟访问文章的admin管理页面通过文章模型的get_admin_url方法获取路径
response = self.client.get(article.get_admin_url())
# 断言文章管理页面访问成功状态码200
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
"""测试用户注册流程:注册请求、注册后用户存在性、邮箱验证、登录与权限升级、内容管理"""
# 注册前断言邮箱为user123@user.com的用户不存在初始状态
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 模拟发送注册请求向account:register接口提交用户名、邮箱、两次一致的密码
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
# 注册后断言邮箱为user123@user.com的用户存在注册成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 获取刚注册的用户,生成邮箱验证链接(使用项目加密逻辑生成签名)
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 双重SHA256加密签名
path = reverse('accounts:result') # 验证结果页的URL路径
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) # 拼接完整验证URL
# 模拟访问邮箱验证链接
response = self.client.get(url)
# 断言验证页面访问成功状态码200
self.assertEqual(response.status_code, 200)
# 模拟刚注册的用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 升级用户权限为超级用户便于测试admin功能
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True # 设为超级用户
user.is_staff = True # 允许登录admin
user.save()
# 删除侧边栏缓存(项目自定义缓存操作,测试中保持原逻辑)
delete_sidebar_cache()
# 创建测试分类(用于后续创建文章)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章(关联升级权限后的用户)
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
# 模拟访问文章的admin管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 模拟用户登出
response = self.client.get(reverse('account:logout'))
# 断言登出请求响应正常允许301/302重定向或200成功
self.assertIn(response.status_code, [301, 302, 200])
# 登出后尝试访问文章admin页面应被拒绝或重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 模拟使用错误密码登录(密码不匹配)
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
# 断言登录请求响应正常(无论成功失败,状态码合法)
self.assertIn(response.status_code, [301, 302, 200])
# 错误登录后尝试访问文章admin页面应被拒绝或重定向
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)
# 验证1使用正确的邮箱和验证码断言无错误返回验证成功
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# 验证2使用错误的邮箱与存储的验证码不匹配断言返回错误信息字符串类型
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
"""测试获取忘记密码验证码的成功场景:提交正确邮箱,返回成功响应"""
# 模拟向account:forget_password_code接口提交正确邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
# 断言请求成功状态码200且返回内容为"ok"(表示验证码发送成功)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
"""测试获取忘记密码验证码的失败场景:无邮箱、邮箱格式错误"""
# 失败场景1不提交邮箱空数据
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
# 断言返回"错误的邮箱"(参数缺失)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 失败场景2提交格式错误的邮箱admin@com不符合标准格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
# 断言返回"错误的邮箱"(格式校验失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""测试忘记密码重置成功场景:提交正确验证码和新密码,密码修改生效"""
# 生成验证码并关联测试用户的邮箱(模拟用户已获取验证码)
code = generate_code()
utils.set_code(self.blog_user.email, code)
# 构造忘记密码重置请求数据:新密码(两次一致)、用户邮箱、正确验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
# 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言重置成功重定向到结果页状态码302
self.assertEqual(resp.status_code, 302)
# 验证密码是否真的修改成功:查询用户并校验新密码
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # 获取用户实例
self.assertNotEqual(blog_user, None) # 断言用户存在
# 使用check_password方法验证新密码是否匹配Django内置密码校验自动处理哈希
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
"""测试忘记密码重置失败场景:使用不存在的邮箱"""
# 构造请求数据:新密码、不存在的邮箱、任意验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
# 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言请求响应正常页面返回错误提示状态码200
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
"""测试忘记密码重置失败场景:验证码错误"""
# 生成正确验证码并关联用户邮箱,但请求时提交错误验证码
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # 错误的验证码
)
# 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
# 断言请求响应正常页面返回验证码错误提示状态码200
self.assertEqual(resp.status_code, 200)

@ -1,49 +0,0 @@
#lm URL路由配置模块
#lm 功能定义用户账户相关的所有URL路径和对应的视图函数映射
#lm 包含登录、注册、登出、密码重置等功能的URL配置
# 导入Django的URL路径定义工具path用于精确路径匹配re_path支持正则表达式匹配
from django.urls import path
from django.urls import re_path
# 导入当前应用accounts的视图模块包含登录、注册等业务逻辑处理
from . import views
# 导入当前应用的自定义登录表单,用于登录页面的表单渲染和验证
from .forms import LoginForm
# 定义应用的命名空间为"accounts"避免不同应用间URL名称冲突
app_name = "accounts"
# URL路由配置列表映射URL路径到对应的视图
urlpatterns = [
#lm 1. 登录页面URL
re_path(r'^login/$', #lm 正则匹配路径:以"login/"开头并结束(即精确匹配"/login/"
views.LoginView.as_view(success_url='/'), #lm 关联LoginView视图登录成功后重定向到网站根路径"/"
name='login', #lm URL的命名用于模板或视图中通过reverse('accounts:login')生成路径
kwargs={'authentication_form': LoginForm}), #lm 传递参数指定登录使用自定义的LoginForm表单
#lm 2. 注册页面URL
re_path(r'^register/$', #lm 正则匹配路径:精确匹配"/register/"
views.RegisterView.as_view(success_url="/"), #lm 关联RegisterView视图注册成功后重定向到网站根路径
name='register'), #lm URL命名用于反向生成注册页面路径
#lm 3. 登出功能URL
re_path(r'^logout/$', #lm 正则匹配路径:精确匹配"/logout/"
views.LogoutView.as_view(), #lm 关联LogoutView视图处理登出逻辑默认登出后重定向到登录页
name='logout'), #lm URL命名用于反向生成登出路径
#lm 4. 账户操作结果页URL如登录/注册/密码重置后的结果提示)
path(r'account/result.html', #lm 精确路径匹配:固定路径"/account/result.html"
views.account_result, #lm 关联普通函数视图account_result处理结果页渲染
name='result'), #lm URL命名用于反向生成结果页路径
#lm 5. 忘记密码页面URL密码重置表单页
re_path(r'^forget_password/$', #lm 正则匹配路径:精确匹配"/forget_password/"
views.ForgetPasswordView.as_view(), #lm 关联ForgetPasswordView视图处理密码重置表单逻辑
name='forget_password'), #lm URL命名用于反向生成忘记密码页面路径
#lm 6. 获取忘记密码验证码的URL发送验证码到邮箱
re_path(r'^forget_password_code/$', #lm 正则匹配路径:精确匹配"/forget_password_code/"
views.ForgetPasswordEmailCode.as_view(), #lm 关联ForgetPasswordEmailCode视图处理发送验证码逻辑
name='forget_password_code'), #lm URL命名用于反向生成获取验证码的路径
]

@ -1,85 +0,0 @@
#lm 用户工具函数模块
#lm 功能:提供用户相关的辅助功能,包括验证码生成、验证、邮件发送等
#lm 封装通用的用户操作逻辑,提高代码复用性
# 导入类型提示模块,用于定义函数参数和返回值的类型(增强代码可读性和类型检查)
import typing
# 导入timedelta用于定义时间间隔此处用于设置验证码有效期
from datetime import timedelta
# 导入Django缓存模块用于临时存储验证码避免数据库频繁读写
from django.core.cache import cache
# 导入Django翻译工具gettext用于实时翻译字符串gettext_lazy用于延迟翻译适合定义常量时使用
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
# 导入项目自定义的发送邮件工具函数,用于实际发送验证码邮件
from djangoblog.utils import send_email
# 定义验证码的有效期5分钟全局常量所有验证码共用此有效期
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""
发送密码重置验证邮件将验证码通过邮件发送给指定邮箱
Args:
to_mail: 接收邮件的目标邮箱地址字符串类型
code: 生成的验证码字符串类型用于后续验证
subject: 邮件主题默认值为"Verify Email"支持国际化可根据语言设置自动翻译
"""
#lm 构造邮件的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}
#lm 调用自定义的send_email函数发送邮件参数为收件人列表、邮件主题、邮件内容
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""
验证用户输入的验证码是否有效与缓存中存储的验证码对比
Args:
email: 用户提交的邮箱地址用于匹配缓存中对应的验证码
code: 用户输入的验证码需要验证的字符串
Return:
验证失败时返回错误提示字符串"Verification code error"验证成功时返回None
Note:
原代码注释说明当前错误处理方式不合理建议通过raise抛出异常替代返回错误字符串
避免调用方需要额外处理返回的错误信息使错误处理更符合Python规范
"""
#lm 从缓存中获取该邮箱对应的验证码调用get_code函数
cache_code = get_code(email)
#lm 对比用户输入的验证码与缓存中的验证码,不一致则返回错误提示
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
"""
将邮箱与对应的验证码存储到Django缓存中并设置过期时间使用全局的_code_ttl
Args:
email: 作为缓存键key的邮箱地址确保每个邮箱的验证码唯一
code: 作为缓存值value的验证码需要存储的字符串
"""
#lm 调用cache.set存储数据key=emailvalue=codetimeout=_code_ttl.seconds有效期转换为秒数
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""
根据邮箱地址从Django缓存中获取对应的验证码
Args:
email: 用于查询的缓存键key即目标邮箱地址
Return:
缓存中存在该邮箱对应的验证码时返回字符串类型的验证码不存在或过期时返回None
"""
#lm 调用cache.get获取缓存值key=email不存在则返回None
return cache.get(email)

@ -1,324 +0,0 @@
#lm 用户视图处理模块
#lm 功能处理用户相关的所有HTTP请求包括登录、注册、登出、密码重置等
#lm 包含类视图和函数视图,实现用户账户管理的核心业务逻辑
# 导入日志模块,用于记录视图操作中的关键信息(如请求类型、用户状态等)
import logging
# 导入Django国际化翻译工具用于视图中文本的多语言支持
from django.utils.translation import gettext_lazy as _
# 导入Django项目配置用于获取SECRET_KEY等全局设置
from django.conf import settings
# 导入Django认证相关模块auth处理登录逻辑REDIRECT_FIELD_NAME定义重定向参数名get_user_model获取自定义用户模型
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 # 登出功能函数
# 导入Django内置认证表单用于登录表单的基础验证
from django.contrib.auth.forms import AuthenticationForm
# 导入密码哈希工具,用于生成加密后的密码(忘记密码功能中更新密码时使用)
from django.contrib.auth.hashers import make_password
# 导入DjangoHTTP响应类重定向、403禁止访问、基础响应
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
# 导入Django快捷函数get_object_or_404获取对象不存在时返回404、render渲染模板
from django.shortcuts import get_object_or_404
from django.shortcuts import render
# 导入reverse函数通过URL名称生成路径避免硬编码路径
from django.urls import reverse
# 导入Django视图装饰器method_decorator为类视图方法添加装饰器、never_cache禁止缓存、csrf_protectCSRF保护、sensitive_post_parameters保护敏感POST参数
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme # 验证重定向地址是否安全
# 导入Django类视图基类View基础视图类、FormView处理表单的视图类、RedirectView处理重定向的视图类
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
# 导入项目自定义工具函数发送邮件、SHA256加密、获取当前站点、生成验证码、删除侧边栏缓存
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
# 导入当前应用accounts的工具函数验证码相关操作
from . import utils
# 导入当前应用的自定义表单:注册、登录、忘记密码、获取忘记密码验证码的表单
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
# 导入当前应用的自定义用户模型
from .models import BlogUser
# 创建日志记录器,用于记录当前视图模块的日志
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
"""
处理用户注册的类视图继承自FormView专门处理表单提交的视图基类
功能展示注册表单验证表单数据创建未激活用户发送邮箱验证链接跳转注册结果页
"""
form_class = RegisterForm #lm 关联的表单类自定义的RegisterForm
template_name = 'account/registration_form.html' #lm 渲染的模板文件路径
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
重写dispatch方法为视图添加CSRF保护装饰器
dispatch是类视图的入口方法所有请求GET/POST都会先经过此方法
"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""
表单数据验证通过后执行的逻辑用户提交的注册信息合法时
"""
if form.is_valid():
#lm 1. 创建用户但不立即提交到数据库commit=False后续手动设置额外字段
user = form.save(False)
#lm 2. 设置用户初始状态:未激活(需邮箱验证后激活)、注册来源为"Register"
user.is_active = False
user.source = 'Register'
#lm 3. 提交用户数据到数据库
user.save(True)
#lm 4. 生成邮箱验证链接包含当前站点域名、验证路径、用户ID、加密签名
site = get_current_site().domain #lm 获取当前站点域名如www.example.com
#lm 双重SHA256加密使用SECRET_KEY+用户ID生成签名防止链接被篡改
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#lm 开发环境DEBUG=True站点域名替换为本地地址127.0.0.1:8000
if settings.DEBUG:
site = '127.0.0.1:8000'
#lm 获取验证结果页的路径通过URL名称反向生成
path = reverse('account:result')
#lm 拼接完整的邮箱验证链接
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
#lm 5. 构造验证邮件内容(包含验证链接)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
#lm 6. 发送验证邮件到用户注册邮箱
send_email(
emailto=[user.email], #lm 收件人列表
title='验证您的电子邮箱', #lm 邮件标题
content=content #lm 邮件HTML内容
)
#lm 7. 重定向到注册结果页携带注册成功的类型和用户ID参数
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
#lm 表单验证失败(如用户名已存在、邮箱格式错误),重新渲染表单并显示错误信息
return self.render_to_response({'form': form})
class LogoutView(RedirectView):
"""
处理用户登出的类视图继承自RedirectView专门处理重定向的视图基类
功能执行登出逻辑删除侧边栏缓存重定向到登录页
"""
url = '/login/' #lm 登出后默认重定向的目标路径(登录页)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""
重写dispatch方法添加禁止缓存装饰器
避免浏览器缓存登出页面防止用户后退到已登出的页面
"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""
处理GET请求用户访问登出URL时
"""
#lm 1. 执行Django内置的登出函数清除用户的session信息
logout(request)
#lm 2. 删除侧边栏缓存(可能存储了用户相关信息,登出后需更新)
delete_sidebar_cache()
#lm 3. 调用父类的get方法执行重定向到登录页
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""
处理用户登录的类视图继承自FormView
功能展示登录表单验证登录信息处理"记住我"功能重定向到目标页面
"""
form_class = LoginForm #lm 关联的表单类自定义的LoginForm
template_name = 'account/login.html' #lm 渲染的登录模板路径
success_url = '/' #lm 登录成功后的默认重定向路径(网站根目录)
redirect_field_name = REDIRECT_FIELD_NAME #lm 重定向参数名(默认是"next"
login_ttl = 2626560 #lm 记住登录状态的有效期(秒),约等于一个月
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""
重写dispatch方法添加三个装饰器
1. sensitive_post_parameters('password')保护密码参数不在错误日志中显示
2. csrf_protect开启CSRF保护防止跨站请求伪造
3. never_cache禁止缓存登录页面确保每次访问都是最新状态
"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""
扩展上下文数据将重定向地址next参数传递到模板中
模板中可根据该参数决定登录成功后跳转到哪里
"""
#lm 从GET请求中获取重定向地址如访问需要登录的页面时会携带next参数
redirect_to = self.request.GET.get(self.redirect_field_name)
#lm 如果没有重定向地址,默认设置为网站根目录
if redirect_to is None:
redirect_to = '/'
#lm 将重定向地址添加到上下文
kwargs['redirect_to'] = redirect_to
#lm 调用父类方法,返回完整的上下文数据
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""
表单数据验证通过后执行的逻辑用户提交的登录信息合法时
注意此处重新初始化了AuthenticationForm用于Django内置的登录验证
"""
#lm 用请求的POST数据和request对象初始化Django内置的认证表单
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
#lm 1. 登录成功,删除侧边栏缓存(可能存储了未登录状态的内容)
delete_sidebar_cache()
#lm 2. 记录重定向参数名到日志(用于调试)
logger.info(self.redirect_field_name)
#lm 3. 执行Django内置的登录函数将用户信息存入session
auth.login(self.request, form.get_user())
#lm 4. 处理"记住我"功能:如果用户勾选了"remember"设置session有效期
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
#lm 5. 调用父类的form_valid方法执行重定向到成功页面
return super(LoginView, self).form_valid(form)
else:
#lm 表单验证失败(如用户名不存在、密码错误),重新渲染表单并显示错误
return self.render_to_response({'form': form})
def get_success_url(self):
"""
自定义登录成功后的重定向地址
优先使用POST请求中的"next"参数如果合法否则使用默认的success_url
"""
#lm 从POST请求中获取重定向地址用户登录时提交的next参数
redirect_to = self.request.POST.get(self.redirect_field_name)
#lm 验证重定向地址是否安全(是否属于当前站点,防止恶意重定向)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]):
#lm 不安全的地址,使用默认的成功重定向路径
redirect_to = self.success_url
return redirect_to
def account_result(request):
"""
处理账户操作结果的函数视图注册成功提示邮箱验证结果
功能根据URL参数type和id展示不同的结果信息激活用户邮箱
"""
#lm 从GET请求中获取操作类型register/validation和用户ID
type = request.GET.get('type')
id = request.GET.get('id')
#lm 根据用户ID查询用户不存在则返回404页面
user = get_object_or_404(get_user_model(), id=id)
#lm 记录操作类型到日志
logger.info(type)
#lm 如果用户已激活is_active=True直接重定向到根目录无需再展示结果
if user.is_active:
return HttpResponseRedirect('/')
#lm 如果操作类型合法属于register或validation处理对应的逻辑
if type and type in ['register', 'validation']:
if type == 'register':
#lm 1. 注册成功场景:展示注册成功提示,告知用户验证邮件已发送
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
#lm 2. 邮箱验证场景:验证签名是否合法,合法则激活用户
#lm 重新生成签名用于与请求中的sign参数对比
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#lm 从GET请求中获取签名参数
sign = request.GET.get('sign')
#lm 如果签名不匹配链接被篡改返回403禁止访问
if sign != c_sign:
return HttpResponseForbidden()
#lm 签名匹配激活用户设置is_active=True并保存
user.is_active = True
user.save()
#lm 展示验证成功提示
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
#lm 渲染结果页面,传递标题和内容
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
#lm 操作类型不合法,重定向到根目录
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
"""
处理忘记密码的类视图继承自FormView
功能展示忘记密码表单验证表单数据验证码邮箱密码更新用户密码
"""
form_class = ForgetPasswordForm #lm 关联的表单类自定义的ForgetPasswordForm
template_name = 'account/forget_password.html' #lm 渲染的忘记密码模板路径
def form_valid(self, form):
"""
表单数据验证通过后执行的逻辑验证码邮箱密码均合法时
"""
if form.is_valid():
#lm 1. 根据表单中的邮箱查询对应的用户
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
#lm 2. 用新密码(加密处理)更新用户密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
#lm 3. 保存密码更新结果到数据库
blog_user.save()
#lm 4. 重定向到登录页(密码重置成功后需重新登录)
return HttpResponseRedirect('/login/')
else:
#lm 表单验证失败(如验证码错误、密码不一致),重新渲染表单并显示错误
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""
处理发送忘记密码验证码的类视图继承自基础View类
功能接收邮箱验证邮箱格式生成验证码发送验证邮件存储验证码到缓存
"""
def post(self, request: HttpRequest):
"""
处理POST请求用户提交邮箱以获取验证码时
"""
#lm 1. 用请求的POST数据初始化验证码表单
form = ForgetPasswordCodeForm(request.POST)
#lm 2. 验证表单(主要验证邮箱格式是否合法)
if not form.is_valid():
#lm 表单验证失败(邮箱格式错误),返回"错误的邮箱"响应
return HttpResponse("错误的邮箱")
#lm 3. 表单验证通过,获取清洗后的邮箱地址
to_email = form.cle

@ -1,280 +0,0 @@
#lm 用户账户功能测试模块
#lm 功能:包含用户注册、登录、密码重置等功能的自动化测试用例
#lm 确保用户相关功能的正确性和稳定性
# 导入Django测试所需的核心类Client模拟HTTP请求、RequestFactory创建请求对象、TestCase测试基类
from django.test import Client, RequestFactory, TestCase
# 导入reverse通过URL名称生成路径用于测试中定位接口
from django.urls import reverse
# 导入timezone处理时间相关字段用于创建测试数据
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
# 定义账户相关测试类继承Django的TestCase提供测试框架支持
class AccountTest(TestCase):
def setUp(self):
"""
测试初始化方法在每个测试方法执行前自动调用
用于创建共用的测试对象避免代码重复
"""
#lm 创建测试客户端用于模拟用户发送HTTP请求
self.client = Client()
#lm 创建请求工厂,用于生成原始请求对象(按需使用)
self.factory = RequestFactory()
#lm 创建普通测试用户用户名test、邮箱admin@admin.com、密码12345678
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
#lm 定义测试用的新密码,后续忘记密码测试中使用
self.new_test = "xxx123--="
def test_validate_account(self):
"""测试账户基础功能超级用户创建、登录验证、admin访问、内容创建与管理"""
#lm 获取当前站点域名(测试中未实际使用,保持原逻辑)
site = get_current_site().domain
#lm 创建超级用户拥有admin管理权限
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
#lm 通过用户名查询刚创建的超级用户,验证创建成功
testuser = BlogUser.objects.get(username='liangliangyy1')
#lm 使用测试客户端模拟超级用户登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
#lm 断言登录成功返回True
self.assertEqual(loginresult, True)
#lm 模拟访问admin后台页面
response = self.client.get('/admin/')
#lm 断言admin页面访问成功状态码200
self.assertEqual(response.status_code, 200)
#lm 创建测试分类:设置名称、创建时间、修改时间并保存
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#lm 创建测试文章:关联作者(超级用户)和分类,设置标题、内容、类型、状态并保存
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' #lm 假设'a'代表普通文章类型
article.status = 'p' #lm 假设'p'代表已发布状态
article.save()
#lm 模拟访问文章的admin管理页面通过文章模型的get_admin_url方法获取路径
response = self.client.get(article.get_admin_url())
#lm 断言文章管理页面访问成功状态码200
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
"""测试用户注册流程:注册请求、注册后用户存在性、邮箱验证、登录与权限升级、内容管理"""
#lm 注册前断言邮箱为user123@user.com的用户不存在初始状态
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lm 模拟发送注册请求向account:register接口提交用户名、邮箱、两次一致的密码
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
#lm 注册后断言邮箱为user123@user.com的用户存在注册成功
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lm 获取刚注册的用户,生成邮箱验证链接(使用项目加密逻辑生成签名)
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) #lm 双重SHA256加密签名
path = reverse('accounts:result') #lm 验证结果页的URL路径
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) #lm 拼接完整验证URL
#lm 模拟访问邮箱验证链接
response = self.client.get(url)
#lm 断言验证页面访问成功状态码200
self.assertEqual(response.status_code, 200)
#lm 模拟刚注册的用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
#lm 升级用户权限为超级用户便于测试admin功能
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True #lm 设为超级用户
user.is_staff = True #lm 允许登录admin
user.save()
#lm 删除侧边栏缓存(项目自定义缓存操作,测试中保持原逻辑)
delete_sidebar_cache()
#lm 创建测试分类(用于后续创建文章)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#lm 创建测试文章(关联升级权限后的用户)
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
#lm 模拟访问文章的admin管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#lm 模拟用户登出
response = self.client.get(reverse('account:logout'))
#lm 断言登出请求响应正常允许301/302重定向或200成功
self.assertIn(response.status_code, [301, 302, 200])
#lm 登出后尝试访问文章admin页面应被拒绝或重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
#lm 模拟使用错误密码登录(密码不匹配)
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
#lm 断言登录请求响应正常(无论成功失败,状态码合法)
self.assertIn(response.status_code, [301, 302, 200])
#lm 错误登录后尝试访问文章admin页面应被拒绝或重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""测试邮箱验证码验证逻辑:正确验证码验证成功,错误邮箱验证失败"""
#lm 定义测试邮箱和生成随机验证码
to_email = "admin@admin.com"
code = generate_code() #lm 调用工具函数生成验证码
#lm 存储验证码(关联邮箱,用于后续验证)
utils.set_code(to_email, code)
#lm 发送验证邮件(测试中仅执行流程,不实际校验邮件发送结果)
utils.send_verify_email(to_email, code)
#lm 验证1使用正确的邮箱和验证码断言无错误返回验证成功
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
#lm 验证2使用错误的邮箱与存储的验证码不匹配断言返回错误信息字符串类型
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
"""测试获取忘记密码验证码的成功场景:提交正确邮箱,返回成功响应"""
#lm 模拟向account:forget_password_code接口提交正确邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
#lm 断言请求成功状态码200且返回内容为"ok"(表示验证码发送成功)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
"""测试获取忘记密码验证码的失败场景:无邮箱、邮箱格式错误"""
#lm 失败场景1不提交邮箱空数据
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
#lm 断言返回"错误的邮箱"(参数缺失)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
#lm 失败场景2提交格式错误的邮箱admin@com不符合标准格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
#lm 断言返回"错误的邮箱"(格式校验失败)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""测试忘记密码重置成功场景:提交正确验证码和新密码,密码修改生效"""
#lm 生成验证码并关联测试用户的邮箱(模拟用户已获取验证码)
code = generate_code()
utils.set_code(self.blog_user.email, code)
#lm 构造忘记密码重置请求数据:新密码(两次一致)、用户邮箱、正确验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
#lm 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lm 断言重置成功重定向到结果页状态码302
self.assertEqual(resp.status_code, 302)
#lm 验证密码是否真的修改成功:查询用户并校验新密码
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() #lm 获取用户实例
self.assertNotEqual(blog_user, None) #lm 断言用户存在
#lm 使用check_password方法验证新密码是否匹配Django内置密码校验自动处理哈希
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
"""测试忘记密码重置失败场景:使用不存在的邮箱"""
#lm 构造请求数据:新密码、不存在的邮箱、任意验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
#lm 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lm 断言请求响应正常页面返回错误提示状态码200
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
"""测试忘记密码重置失败场景:验证码错误"""
#lm 生成正确验证码并关联用户邮箱,但请求时提交错误验证码
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", #lm 错误的验证码
)
#lm 模拟发送密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lm 断言请求响应正常页面返回验证码错误提示状态码200
self.assertEqual(resp.status_code, 200)
Loading…
Cancel
Save